mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(subscriptions): Add client secret auth support in subscriptions APIs (#9713)
Co-authored-by: Prajjwal kumar <write2prajjwal@gmail.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Prajjwal Kumar <prajjwal.kumar@juspay.in> Co-authored-by: Jagan <jaganelavarasan@gmail.com>
This commit is contained in:
@ -74,6 +74,12 @@ pub struct SubscriptionResponse {
|
|||||||
|
|
||||||
/// Optional customer ID associated with this subscription.
|
/// Optional customer ID associated with this subscription.
|
||||||
pub customer_id: common_utils::id_type::CustomerId,
|
pub customer_id: common_utils::id_type::CustomerId,
|
||||||
|
|
||||||
|
/// Payment details for the invoice.
|
||||||
|
pub payment: Option<PaymentResponseData>,
|
||||||
|
|
||||||
|
/// Invoice Details for the subscription.
|
||||||
|
pub invoice: Option<Invoice>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Possible states of a subscription lifecycle.
|
/// Possible states of a subscription lifecycle.
|
||||||
@ -127,6 +133,8 @@ impl SubscriptionResponse {
|
|||||||
merchant_id: common_utils::id_type::MerchantId,
|
merchant_id: common_utils::id_type::MerchantId,
|
||||||
client_secret: Option<Secret<String>>,
|
client_secret: Option<Secret<String>>,
|
||||||
customer_id: common_utils::id_type::CustomerId,
|
customer_id: common_utils::id_type::CustomerId,
|
||||||
|
payment: Option<PaymentResponseData>,
|
||||||
|
invoice: Option<Invoice>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@ -138,6 +146,8 @@ impl SubscriptionResponse {
|
|||||||
merchant_id,
|
merchant_id,
|
||||||
coupon_code: None,
|
coupon_code: None,
|
||||||
customer_id,
|
customer_id,
|
||||||
|
payment,
|
||||||
|
invoice,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,6 +191,10 @@ impl ClientSecret {
|
|||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_string(&self) -> &String {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||||
@ -197,6 +211,7 @@ impl ApiEventMetric for GetPlansResponse {}
|
|||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||||
pub struct ConfirmSubscriptionPaymentDetails {
|
pub struct ConfirmSubscriptionPaymentDetails {
|
||||||
|
pub shipping: Option<Address>,
|
||||||
pub payment_method: api_enums::PaymentMethod,
|
pub payment_method: api_enums::PaymentMethod,
|
||||||
pub payment_method_type: Option<api_enums::PaymentMethodType>,
|
pub payment_method_type: Option<api_enums::PaymentMethodType>,
|
||||||
pub payment_method_data: PaymentMethodDataRequest,
|
pub payment_method_data: PaymentMethodDataRequest,
|
||||||
@ -278,7 +293,7 @@ pub struct PaymentResponseData {
|
|||||||
pub error_code: Option<String>,
|
pub error_code: Option<String>,
|
||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
pub payment_method_type: Option<api_enums::PaymentMethodType>,
|
pub payment_method_type: Option<api_enums::PaymentMethodType>,
|
||||||
pub client_secret: Option<String>,
|
pub client_secret: Option<Secret<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||||
@ -294,7 +309,7 @@ pub struct CreateMitPaymentRequestData {
|
|||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||||
pub struct ConfirmSubscriptionRequest {
|
pub struct ConfirmSubscriptionRequest {
|
||||||
/// Client secret for SDK based interaction.
|
/// Client secret for SDK based interaction.
|
||||||
pub client_secret: Option<String>,
|
pub client_secret: Option<ClientSecret>,
|
||||||
|
|
||||||
/// Identifier for the associated plan_id.
|
/// Identifier for the associated plan_id.
|
||||||
pub plan_id: Option<String>,
|
pub plan_id: Option<String>,
|
||||||
@ -305,15 +320,6 @@ pub struct ConfirmSubscriptionRequest {
|
|||||||
/// Idenctifier for the coupon code for the subscription.
|
/// Idenctifier for the coupon code for the subscription.
|
||||||
pub coupon_code: Option<String>,
|
pub coupon_code: Option<String>,
|
||||||
|
|
||||||
/// Identifier for customer.
|
|
||||||
pub customer_id: common_utils::id_type::CustomerId,
|
|
||||||
|
|
||||||
/// Billing address for the subscription.
|
|
||||||
pub billing: Option<Address>,
|
|
||||||
|
|
||||||
/// Shipping address for the subscription.
|
|
||||||
pub shipping: Option<Address>,
|
|
||||||
|
|
||||||
/// Payment details for the invoice.
|
/// Payment details for the invoice.
|
||||||
pub payment_details: ConfirmSubscriptionPaymentDetails,
|
pub payment_details: ConfirmSubscriptionPaymentDetails,
|
||||||
}
|
}
|
||||||
@ -328,11 +334,15 @@ impl ConfirmSubscriptionRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_billing_address(&self) -> Result<Address, error_stack::Report<ValidationError>> {
|
pub fn get_billing_address(&self) -> Result<Address, error_stack::Report<ValidationError>> {
|
||||||
self.billing.clone().ok_or(error_stack::report!(
|
self.payment_details
|
||||||
ValidationError::MissingRequiredField {
|
.payment_method_data
|
||||||
field_name: "billing".to_string()
|
.billing
|
||||||
}
|
.clone()
|
||||||
))
|
.ok_or(error_stack::report!(
|
||||||
|
ValidationError::MissingRequiredField {
|
||||||
|
field_name: "billing".to_string()
|
||||||
|
}
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ pub struct InvoiceNew {
|
|||||||
pub metadata: Option<SecretSerdeValue>,
|
pub metadata: Option<SecretSerdeValue>,
|
||||||
pub created_at: time::PrimitiveDateTime,
|
pub created_at: time::PrimitiveDateTime,
|
||||||
pub modified_at: time::PrimitiveDateTime,
|
pub modified_at: time::PrimitiveDateTime,
|
||||||
|
pub connector_invoice_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
@ -49,6 +50,7 @@ pub struct Invoice {
|
|||||||
pub metadata: Option<SecretSerdeValue>,
|
pub metadata: Option<SecretSerdeValue>,
|
||||||
pub created_at: time::PrimitiveDateTime,
|
pub created_at: time::PrimitiveDateTime,
|
||||||
pub modified_at: time::PrimitiveDateTime,
|
pub modified_at: time::PrimitiveDateTime,
|
||||||
|
pub connector_invoice_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, AsChangeset, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, AsChangeset, Deserialize)]
|
||||||
@ -56,6 +58,7 @@ pub struct Invoice {
|
|||||||
pub struct InvoiceUpdate {
|
pub struct InvoiceUpdate {
|
||||||
pub status: Option<String>,
|
pub status: Option<String>,
|
||||||
pub payment_method_id: Option<String>,
|
pub payment_method_id: Option<String>,
|
||||||
|
pub connector_invoice_id: Option<String>,
|
||||||
pub modified_at: time::PrimitiveDateTime,
|
pub modified_at: time::PrimitiveDateTime,
|
||||||
pub payment_intent_id: Option<common_utils::id_type::PaymentId>,
|
pub payment_intent_id: Option<common_utils::id_type::PaymentId>,
|
||||||
}
|
}
|
||||||
@ -75,6 +78,7 @@ impl InvoiceNew {
|
|||||||
status: InvoiceStatus,
|
status: InvoiceStatus,
|
||||||
provider_name: Connector,
|
provider_name: Connector,
|
||||||
metadata: Option<SecretSerdeValue>,
|
metadata: Option<SecretSerdeValue>,
|
||||||
|
connector_invoice_id: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let id = common_utils::id_type::InvoiceId::generate();
|
let id = common_utils::id_type::InvoiceId::generate();
|
||||||
let now = common_utils::date_time::now();
|
let now = common_utils::date_time::now();
|
||||||
@ -94,6 +98,7 @@ impl InvoiceNew {
|
|||||||
metadata,
|
metadata,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
modified_at: now,
|
modified_at: now,
|
||||||
|
connector_invoice_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,11 +107,13 @@ impl InvoiceUpdate {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
payment_method_id: Option<String>,
|
payment_method_id: Option<String>,
|
||||||
status: Option<InvoiceStatus>,
|
status: Option<InvoiceStatus>,
|
||||||
|
connector_invoice_id: Option<String>,
|
||||||
payment_intent_id: Option<common_utils::id_type::PaymentId>,
|
payment_intent_id: Option<common_utils::id_type::PaymentId>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
payment_method_id,
|
payment_method_id,
|
||||||
status: status.map(|status| status.to_string()),
|
status: status.map(|status| status.to_string()),
|
||||||
|
connector_invoice_id,
|
||||||
payment_intent_id,
|
payment_intent_id,
|
||||||
modified_at: common_utils::date_time::now(),
|
modified_at: common_utils::date_time::now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -750,6 +750,8 @@ diesel::table! {
|
|||||||
metadata -> Nullable<Jsonb>,
|
metadata -> Nullable<Jsonb>,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
modified_at -> Timestamp,
|
modified_at -> Timestamp,
|
||||||
|
#[max_length = 64]
|
||||||
|
connector_invoice_id -> Nullable<Varchar>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -762,6 +762,8 @@ diesel::table! {
|
|||||||
metadata -> Nullable<Jsonb>,
|
metadata -> Nullable<Jsonb>,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
modified_at -> Timestamp,
|
modified_at -> Timestamp,
|
||||||
|
#[max_length = 64]
|
||||||
|
connector_invoice_id -> Nullable<Varchar>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,7 @@ use api_models::subscription::{
|
|||||||
use common_enums::connector_enums;
|
use common_enums::connector_enums;
|
||||||
use common_utils::id_type::GenerateId;
|
use common_utils::id_type::GenerateId;
|
||||||
use error_stack::ResultExt;
|
use error_stack::ResultExt;
|
||||||
use hyperswitch_domain_models::{
|
use hyperswitch_domain_models::{api::ApplicationResponse, merchant_context::MerchantContext};
|
||||||
api::ApplicationResponse, merchant_context::MerchantContext,
|
|
||||||
router_response_types::subscriptions as subscription_response_types,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::errors::{self, RouterResponse};
|
use super::errors::{self, RouterResponse};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -31,7 +28,7 @@ pub async fn create_subscription(
|
|||||||
merchant_context: MerchantContext,
|
merchant_context: MerchantContext,
|
||||||
profile_id: common_utils::id_type::ProfileId,
|
profile_id: common_utils::id_type::ProfileId,
|
||||||
request: subscription_types::CreateSubscriptionRequest,
|
request: subscription_types::CreateSubscriptionRequest,
|
||||||
) -> RouterResponse<subscription_types::ConfirmSubscriptionResponse> {
|
) -> RouterResponse<SubscriptionResponse> {
|
||||||
let subscription_id = common_utils::id_type::SubscriptionId::generate();
|
let subscription_id = common_utils::id_type::SubscriptionId::generate();
|
||||||
|
|
||||||
let profile =
|
let profile =
|
||||||
@ -68,7 +65,7 @@ pub async fn create_subscription(
|
|||||||
.create_payment_with_confirm_false(subscription.handler.state, &request)
|
.create_payment_with_confirm_false(subscription.handler.state, &request)
|
||||||
.await
|
.await
|
||||||
.attach_printable("subscriptions: failed to create payment")?;
|
.attach_printable("subscriptions: failed to create payment")?;
|
||||||
let invoice_entry = invoice_handler
|
let invoice = invoice_handler
|
||||||
.create_invoice_entry(
|
.create_invoice_entry(
|
||||||
&state,
|
&state,
|
||||||
billing_handler.merchant_connector_id,
|
billing_handler.merchant_connector_id,
|
||||||
@ -78,6 +75,7 @@ pub async fn create_subscription(
|
|||||||
connector_enums::InvoiceStatus::InvoiceCreated,
|
connector_enums::InvoiceStatus::InvoiceCreated,
|
||||||
billing_handler.connector_data.connector_name,
|
billing_handler.connector_data.connector_name,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.attach_printable("subscriptions: failed to create invoice")?;
|
.attach_printable("subscriptions: failed to create invoice")?;
|
||||||
@ -91,11 +89,7 @@ pub async fn create_subscription(
|
|||||||
.await
|
.await
|
||||||
.attach_printable("subscriptions: failed to update subscription")?;
|
.attach_printable("subscriptions: failed to update subscription")?;
|
||||||
|
|
||||||
let response = subscription.generate_response(
|
let response = subscription.to_subscription_response(Some(payment), Some(&invoice))?;
|
||||||
&invoice_entry,
|
|
||||||
&payment,
|
|
||||||
subscription_response_types::SubscriptionStatus::Created,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(ApplicationResponse::Json(response))
|
Ok(ApplicationResponse::Json(response))
|
||||||
}
|
}
|
||||||
@ -148,14 +142,12 @@ pub async fn get_subscription_plans(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(subscription_types::SubscriptionPlanPrices::from)
|
.map(subscription_types::SubscriptionPlanPrices::from)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ApplicationResponse::Json(response))
|
Ok(ApplicationResponse::Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates and confirms a subscription in one operation.
|
/// Creates and confirms a subscription in one operation.
|
||||||
/// This method combines the creation and confirmation flow to reduce API calls
|
|
||||||
pub async fn create_and_confirm_subscription(
|
pub async fn create_and_confirm_subscription(
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
merchant_context: MerchantContext,
|
merchant_context: MerchantContext,
|
||||||
@ -163,7 +155,6 @@ pub async fn create_and_confirm_subscription(
|
|||||||
request: subscription_types::CreateAndConfirmSubscriptionRequest,
|
request: subscription_types::CreateAndConfirmSubscriptionRequest,
|
||||||
) -> RouterResponse<subscription_types::ConfirmSubscriptionResponse> {
|
) -> RouterResponse<subscription_types::ConfirmSubscriptionResponse> {
|
||||||
let subscription_id = common_utils::id_type::SubscriptionId::generate();
|
let subscription_id = common_utils::id_type::SubscriptionId::generate();
|
||||||
|
|
||||||
let profile =
|
let profile =
|
||||||
SubscriptionHandler::find_business_profile(&state, &merchant_context, &profile_id)
|
SubscriptionHandler::find_business_profile(&state, &merchant_context, &profile_id)
|
||||||
.await
|
.await
|
||||||
@ -240,6 +231,9 @@ pub async fn create_and_confirm_subscription(
|
|||||||
.unwrap_or(connector_enums::InvoiceStatus::InvoiceCreated),
|
.unwrap_or(connector_enums::InvoiceStatus::InvoiceCreated),
|
||||||
billing_handler.connector_data.connector_name,
|
billing_handler.connector_data.connector_name,
|
||||||
None,
|
None,
|
||||||
|
invoice_details
|
||||||
|
.clone()
|
||||||
|
.map(|invoice| invoice.id.get_string_repr().to_string()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -292,13 +286,23 @@ pub async fn confirm_subscription(
|
|||||||
SubscriptionHandler::find_business_profile(&state, &merchant_context, &profile_id)
|
SubscriptionHandler::find_business_profile(&state, &merchant_context, &profile_id)
|
||||||
.await
|
.await
|
||||||
.attach_printable("subscriptions: failed to find business profile")?;
|
.attach_printable("subscriptions: failed to find business profile")?;
|
||||||
let customer =
|
|
||||||
SubscriptionHandler::find_customer(&state, &merchant_context, &request.customer_id)
|
|
||||||
.await
|
|
||||||
.attach_printable("subscriptions: failed to find customer")?;
|
|
||||||
|
|
||||||
let handler = SubscriptionHandler::new(&state, &merchant_context);
|
let handler = SubscriptionHandler::new(&state, &merchant_context);
|
||||||
|
if let Some(client_secret) = request.client_secret.clone() {
|
||||||
|
handler
|
||||||
|
.find_and_validate_subscription(&client_secret.into())
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
let mut subscription_entry = handler.find_subscription(subscription_id).await?;
|
let mut subscription_entry = handler.find_subscription(subscription_id).await?;
|
||||||
|
let customer = SubscriptionHandler::find_customer(
|
||||||
|
&state,
|
||||||
|
&merchant_context,
|
||||||
|
&subscription_entry.subscription.customer_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.attach_printable("subscriptions: failed to find customer")?;
|
||||||
|
|
||||||
let invoice_handler = subscription_entry.get_invoice_handler(profile.clone());
|
let invoice_handler = subscription_entry.get_invoice_handler(profile.clone());
|
||||||
let invoice = invoice_handler
|
let invoice = invoice_handler
|
||||||
.get_latest_invoice(&state)
|
.get_latest_invoice(&state)
|
||||||
@ -331,7 +335,7 @@ pub async fn confirm_subscription(
|
|||||||
.create_customer_on_connector(
|
.create_customer_on_connector(
|
||||||
&state,
|
&state,
|
||||||
subscription.customer_id.clone(),
|
subscription.customer_id.clone(),
|
||||||
request.billing.clone(),
|
request.payment_details.payment_method_data.billing.clone(),
|
||||||
request
|
request
|
||||||
.payment_details
|
.payment_details
|
||||||
.payment_method_data
|
.payment_method_data
|
||||||
@ -344,7 +348,7 @@ pub async fn confirm_subscription(
|
|||||||
&state,
|
&state,
|
||||||
subscription,
|
subscription,
|
||||||
request.item_price_id,
|
request.item_price_id,
|
||||||
request.billing,
|
request.payment_details.payment_method_data.billing,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -359,6 +363,9 @@ pub async fn confirm_subscription(
|
|||||||
.clone()
|
.clone()
|
||||||
.and_then(|invoice| invoice.status)
|
.and_then(|invoice| invoice.status)
|
||||||
.unwrap_or(connector_enums::InvoiceStatus::InvoiceCreated),
|
.unwrap_or(connector_enums::InvoiceStatus::InvoiceCreated),
|
||||||
|
invoice_details
|
||||||
|
.clone()
|
||||||
|
.map(|invoice| invoice.id.get_string_repr().to_string()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -418,9 +425,9 @@ pub async fn get_subscription(
|
|||||||
.await
|
.await
|
||||||
.attach_printable("subscriptions: failed to get subscription entry in get_subscription")?;
|
.attach_printable("subscriptions: failed to get subscription entry in get_subscription")?;
|
||||||
|
|
||||||
Ok(ApplicationResponse::Json(
|
let response = subscription.to_subscription_response(None, None)?;
|
||||||
subscription.to_subscription_response(),
|
|
||||||
))
|
Ok(ApplicationResponse::Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_estimate(
|
pub async fn get_estimate(
|
||||||
|
|||||||
@ -44,6 +44,7 @@ impl InvoiceHandler {
|
|||||||
status: connector_enums::InvoiceStatus,
|
status: connector_enums::InvoiceStatus,
|
||||||
provider_name: connector_enums::Connector,
|
provider_name: connector_enums::Connector,
|
||||||
metadata: Option<pii::SecretSerdeValue>,
|
metadata: Option<pii::SecretSerdeValue>,
|
||||||
|
connector_invoice_id: Option<String>,
|
||||||
) -> errors::RouterResult<diesel_models::invoice::Invoice> {
|
) -> errors::RouterResult<diesel_models::invoice::Invoice> {
|
||||||
let invoice_new = diesel_models::invoice::InvoiceNew::new(
|
let invoice_new = diesel_models::invoice::InvoiceNew::new(
|
||||||
self.subscription.id.to_owned(),
|
self.subscription.id.to_owned(),
|
||||||
@ -58,6 +59,7 @@ impl InvoiceHandler {
|
|||||||
status,
|
status,
|
||||||
provider_name,
|
provider_name,
|
||||||
metadata,
|
metadata,
|
||||||
|
connector_invoice_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let invoice = state
|
let invoice = state
|
||||||
@ -79,10 +81,12 @@ impl InvoiceHandler {
|
|||||||
payment_method_id: Option<Secret<String>>,
|
payment_method_id: Option<Secret<String>>,
|
||||||
payment_intent_id: Option<common_utils::id_type::PaymentId>,
|
payment_intent_id: Option<common_utils::id_type::PaymentId>,
|
||||||
status: connector_enums::InvoiceStatus,
|
status: connector_enums::InvoiceStatus,
|
||||||
|
connector_invoice_id: Option<String>,
|
||||||
) -> errors::RouterResult<diesel_models::invoice::Invoice> {
|
) -> errors::RouterResult<diesel_models::invoice::Invoice> {
|
||||||
let update_invoice = diesel_models::invoice::InvoiceUpdate::new(
|
let update_invoice = diesel_models::invoice::InvoiceUpdate::new(
|
||||||
payment_method_id.as_ref().map(|id| id.peek()).cloned(),
|
payment_method_id.as_ref().map(|id| id.peek()).cloned(),
|
||||||
Some(status),
|
Some(status),
|
||||||
|
connector_invoice_id,
|
||||||
payment_intent_id,
|
payment_intent_id,
|
||||||
);
|
);
|
||||||
state
|
state
|
||||||
@ -189,8 +193,8 @@ impl InvoiceHandler {
|
|||||||
) -> errors::RouterResult<subscription_types::PaymentResponseData> {
|
) -> errors::RouterResult<subscription_types::PaymentResponseData> {
|
||||||
let payment_details = &request.payment_details;
|
let payment_details = &request.payment_details;
|
||||||
let cit_payment_request = subscription_types::ConfirmPaymentsRequestData {
|
let cit_payment_request = subscription_types::ConfirmPaymentsRequestData {
|
||||||
billing: request.billing.clone(),
|
billing: request.payment_details.payment_method_data.billing.clone(),
|
||||||
shipping: request.shipping.clone(),
|
shipping: request.payment_details.shipping.clone(),
|
||||||
payment_method: payment_details.payment_method,
|
payment_method: payment_details.payment_method,
|
||||||
payment_method_type: payment_details.payment_method_type,
|
payment_method_type: payment_details.payment_method_type,
|
||||||
payment_method_data: payment_details.payment_method_data.clone(),
|
payment_method_data: payment_details.payment_method_data.clone(),
|
||||||
|
|||||||
@ -19,7 +19,7 @@ use crate::{
|
|||||||
core::{errors::StorageErrorExt, subscription::invoice_handler::InvoiceHandler},
|
core::{errors::StorageErrorExt, subscription::invoice_handler::InvoiceHandler},
|
||||||
db::CustomResult,
|
db::CustomResult,
|
||||||
routes::SessionState,
|
routes::SessionState,
|
||||||
types::domain,
|
types::{domain, transformers::ForeignTryFrom},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SubscriptionHandler<'a> {
|
pub struct SubscriptionHandler<'a> {
|
||||||
@ -227,31 +227,16 @@ impl SubscriptionWithHandler<'_> {
|
|||||||
price_id: None,
|
price_id: None,
|
||||||
coupon: None,
|
coupon: None,
|
||||||
billing_processor_subscription_id: self.subscription.connector_subscription_id.clone(),
|
billing_processor_subscription_id: self.subscription.connector_subscription_id.clone(),
|
||||||
invoice: Some(subscription_types::Invoice {
|
invoice: Some(subscription_types::Invoice::foreign_try_from(invoice)?),
|
||||||
id: invoice.id.clone(),
|
|
||||||
subscription_id: invoice.subscription_id.clone(),
|
|
||||||
merchant_id: invoice.merchant_id.clone(),
|
|
||||||
profile_id: invoice.profile_id.clone(),
|
|
||||||
merchant_connector_id: invoice.merchant_connector_id.clone(),
|
|
||||||
payment_intent_id: invoice.payment_intent_id.clone(),
|
|
||||||
payment_method_id: invoice.payment_method_id.clone(),
|
|
||||||
customer_id: invoice.customer_id.clone(),
|
|
||||||
amount: invoice.amount,
|
|
||||||
currency: api_enums::Currency::from_str(invoice.currency.as_str())
|
|
||||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
|
||||||
field_name: "currency",
|
|
||||||
})
|
|
||||||
.attach_printable(format!(
|
|
||||||
"unable to parse currency name {currency:?}",
|
|
||||||
currency = invoice.currency
|
|
||||||
))?,
|
|
||||||
status: invoice.status.clone(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_subscription_response(&self) -> SubscriptionResponse {
|
pub fn to_subscription_response(
|
||||||
SubscriptionResponse::new(
|
&self,
|
||||||
|
payment: Option<subscription_types::PaymentResponseData>,
|
||||||
|
invoice: Option<&diesel_models::invoice::Invoice>,
|
||||||
|
) -> errors::RouterResult<SubscriptionResponse> {
|
||||||
|
Ok(SubscriptionResponse::new(
|
||||||
self.subscription.id.clone(),
|
self.subscription.id.clone(),
|
||||||
self.subscription.merchant_reference_id.clone(),
|
self.subscription.merchant_reference_id.clone(),
|
||||||
SubscriptionStatus::from_str(&self.subscription.status)
|
SubscriptionStatus::from_str(&self.subscription.status)
|
||||||
@ -261,7 +246,15 @@ impl SubscriptionWithHandler<'_> {
|
|||||||
self.subscription.merchant_id.to_owned(),
|
self.subscription.merchant_id.to_owned(),
|
||||||
self.subscription.client_secret.clone().map(Secret::new),
|
self.subscription.client_secret.clone().map(Secret::new),
|
||||||
self.subscription.customer_id.clone(),
|
self.subscription.customer_id.clone(),
|
||||||
)
|
payment,
|
||||||
|
invoice
|
||||||
|
.map(
|
||||||
|
|invoice| -> errors::RouterResult<subscription_types::Invoice> {
|
||||||
|
subscription_types::Invoice::foreign_try_from(invoice)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.transpose()?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_subscription(
|
pub async fn update_subscription(
|
||||||
@ -369,3 +362,30 @@ impl SubscriptionWithHandler<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ForeignTryFrom<&diesel_models::invoice::Invoice> for subscription_types::Invoice {
|
||||||
|
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||||
|
|
||||||
|
fn foreign_try_from(invoice: &diesel_models::invoice::Invoice) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
id: invoice.id.clone(),
|
||||||
|
subscription_id: invoice.subscription_id.clone(),
|
||||||
|
merchant_id: invoice.merchant_id.clone(),
|
||||||
|
profile_id: invoice.profile_id.clone(),
|
||||||
|
merchant_connector_id: invoice.merchant_connector_id.clone(),
|
||||||
|
payment_intent_id: invoice.payment_intent_id.clone(),
|
||||||
|
payment_method_id: invoice.payment_method_id.clone(),
|
||||||
|
customer_id: invoice.customer_id.clone(),
|
||||||
|
amount: invoice.amount,
|
||||||
|
currency: api_enums::Currency::from_str(invoice.currency.as_str())
|
||||||
|
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||||
|
field_name: "currency",
|
||||||
|
})
|
||||||
|
.attach_printable(format!(
|
||||||
|
"unable to parse currency name {currency:?}",
|
||||||
|
currency = invoice.currency
|
||||||
|
))?,
|
||||||
|
status: invoice.status.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2645,6 +2645,7 @@ async fn subscription_incoming_webhook_flow(
|
|||||||
InvoiceStatus::PaymentPending,
|
InvoiceStatus::PaymentPending,
|
||||||
connector,
|
connector,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -2664,6 +2665,7 @@ async fn subscription_incoming_webhook_flow(
|
|||||||
payment_response.payment_method_id.clone(),
|
payment_response.payment_method_id.clone(),
|
||||||
Some(payment_response.payment_id.clone()),
|
Some(payment_response.payment_id.clone()),
|
||||||
InvoiceStatus::from(payment_response.status),
|
InvoiceStatus::from(payment_response.status),
|
||||||
|
Some(mit_payment_data.invoice_id.get_string_repr().to_string()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@ -102,16 +102,25 @@ pub async fn confirm_subscription(
|
|||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let flow = Flow::ConfirmSubscription;
|
let flow = Flow::ConfirmSubscription;
|
||||||
let subscription_id = subscription_id.into_inner();
|
let subscription_id = subscription_id.into_inner();
|
||||||
|
let payload = json_payload.into_inner();
|
||||||
let profile_id = match extract_profile_id(&req) {
|
let profile_id = match extract_profile_id(&req) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(response) => return response,
|
Err(response) => return response,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let api_auth = auth::ApiKeyAuth::default();
|
||||||
|
|
||||||
|
let (auth_type, _) =
|
||||||
|
match auth::check_client_secret_and_get_auth(req.headers(), &payload, api_auth) {
|
||||||
|
Ok(auth) => auth,
|
||||||
|
Err(err) => return oss_api::log_and_return_error_response(error_stack::report!(err)),
|
||||||
|
};
|
||||||
|
|
||||||
Box::pin(oss_api::server_wrap(
|
Box::pin(oss_api::server_wrap(
|
||||||
flow,
|
flow,
|
||||||
state,
|
state,
|
||||||
&req,
|
&req,
|
||||||
json_payload.into_inner(),
|
payload,
|
||||||
|state, auth: auth::AuthenticationData, payload, _| {
|
|state, auth: auth::AuthenticationData, payload, _| {
|
||||||
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
|
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
|
||||||
domain::Context(auth.merchant_account, auth.key_store),
|
domain::Context(auth.merchant_account, auth.key_store),
|
||||||
@ -125,10 +134,7 @@ pub async fn confirm_subscription(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
auth::auth_type(
|
auth::auth_type(
|
||||||
&auth::HeaderAuth(auth::ApiKeyAuth {
|
&*auth_type,
|
||||||
is_connected_allowed: false,
|
|
||||||
is_platform_allowed: false,
|
|
||||||
}),
|
|
||||||
&auth::JWTAuth {
|
&auth::JWTAuth {
|
||||||
permission: Permission::ProfileSubscriptionWrite,
|
permission: Permission::ProfileSubscriptionWrite,
|
||||||
},
|
},
|
||||||
@ -147,28 +153,36 @@ pub async fn get_subscription_plans(
|
|||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let flow = Flow::GetPlansForSubscription;
|
let flow = Flow::GetPlansForSubscription;
|
||||||
let api_auth = auth::ApiKeyAuth::default();
|
let api_auth = auth::ApiKeyAuth::default();
|
||||||
|
let payload = query.into_inner();
|
||||||
|
|
||||||
let profile_id = match extract_profile_id(&req) {
|
let profile_id = match extract_profile_id(&req) {
|
||||||
Ok(profile_id) => profile_id,
|
Ok(profile_id) => profile_id,
|
||||||
Err(response) => return response,
|
Err(response) => return response,
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth_data = match auth::is_ephemeral_auth(req.headers(), api_auth) {
|
let (auth_type, _) =
|
||||||
Ok(auth) => auth,
|
match auth::check_client_secret_and_get_auth(req.headers(), &payload, api_auth) {
|
||||||
Err(err) => return crate::services::api::log_and_return_error_response(err),
|
Ok(auth) => auth,
|
||||||
};
|
Err(err) => return oss_api::log_and_return_error_response(error_stack::report!(err)),
|
||||||
|
};
|
||||||
Box::pin(oss_api::server_wrap(
|
Box::pin(oss_api::server_wrap(
|
||||||
flow,
|
flow,
|
||||||
state,
|
state,
|
||||||
&req,
|
&req,
|
||||||
query.into_inner(),
|
payload,
|
||||||
|state, auth: auth::AuthenticationData, query, _| {
|
|state, auth: auth::AuthenticationData, query, _| {
|
||||||
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
|
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
|
||||||
domain::Context(auth.merchant_account, auth.key_store),
|
domain::Context(auth.merchant_account, auth.key_store),
|
||||||
));
|
));
|
||||||
subscription::get_subscription_plans(state, merchant_context, profile_id.clone(), query)
|
subscription::get_subscription_plans(state, merchant_context, profile_id.clone(), query)
|
||||||
},
|
},
|
||||||
&*auth_data,
|
auth::auth_type(
|
||||||
|
&*auth_type,
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::ProfileSubscriptionRead,
|
||||||
|
},
|
||||||
|
req.headers(),
|
||||||
|
),
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
@ -187,6 +201,7 @@ pub async fn get_subscription(
|
|||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(response) => return response,
|
Err(response) => return response,
|
||||||
};
|
};
|
||||||
|
|
||||||
Box::pin(oss_api::server_wrap(
|
Box::pin(oss_api::server_wrap(
|
||||||
flow,
|
flow,
|
||||||
state,
|
state,
|
||||||
@ -245,10 +260,16 @@ pub async fn create_and_confirm_subscription(
|
|||||||
payload.clone(),
|
payload.clone(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
&auth::HeaderAuth(auth::ApiKeyAuth {
|
auth::auth_type(
|
||||||
is_connected_allowed: false,
|
&auth::HeaderAuth(auth::ApiKeyAuth {
|
||||||
is_platform_allowed: false,
|
is_connected_allowed: false,
|
||||||
}),
|
is_platform_allowed: false,
|
||||||
|
}),
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::ProfileSubscriptionWrite,
|
||||||
|
},
|
||||||
|
req.headers(),
|
||||||
|
),
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -4326,6 +4326,20 @@ impl ClientSecretFetch for api_models::payment_methods::PaymentMethodUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "v1")]
|
||||||
|
impl ClientSecretFetch for api_models::subscription::ConfirmSubscriptionRequest {
|
||||||
|
fn get_client_secret(&self) -> Option<&String> {
|
||||||
|
self.client_secret.as_ref().map(|s| s.as_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "v1")]
|
||||||
|
impl ClientSecretFetch for api_models::subscription::GetPlansQuery {
|
||||||
|
fn get_client_secret(&self) -> Option<&String> {
|
||||||
|
self.client_secret.as_ref().map(|s| s.as_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "v1")]
|
#[cfg(feature = "v1")]
|
||||||
impl ClientSecretFetch for api_models::authentication::AuthenticationEligibilityRequest {
|
impl ClientSecretFetch for api_models::authentication::AuthenticationEligibilityRequest {
|
||||||
fn get_client_secret(&self) -> Option<&String> {
|
fn get_client_secret(&self) -> Option<&String> {
|
||||||
|
|||||||
@ -204,6 +204,7 @@ impl<'a> InvoiceSyncHandler<'a> {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
common_enums::connector_enums::InvoiceStatus::from(invoice_sync_status),
|
common_enums::connector_enums::InvoiceStatus::from(invoice_sync_status),
|
||||||
|
Some(connector_invoice_id),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.attach_printable("Failed to update invoice in DB")?;
|
.attach_printable("Failed to update invoice in DB")?;
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
ALTER TABLE invoice DROP COLUMN IF EXISTS connector_invoice_id;
|
||||||
|
DROP INDEX IF EXISTS invoice_subscription_id_connector_invoice_id_index;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
ALTER TABLE invoice ADD COLUMN IF NOT EXISTS connector_invoice_id VARCHAR(64);
|
||||||
|
CREATE INDEX invoice_subscription_id_connector_invoice_id_index ON invoice (subscription_id, connector_invoice_id);
|
||||||
Reference in New Issue
Block a user