diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index ae617c5930..0f1ed92fc3 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -237,6 +237,7 @@ pub struct ConnectorConfig { pub stripe: Option, #[cfg(feature = "payouts")] pub stripe_payout: Option, + // pub stripebilling : Option, pub signifyd: Option, pub trustpay: Option, pub threedsecureio: Option, @@ -408,6 +409,7 @@ impl ConnectorConfig { Connector::Square => Ok(connector_data.square), Connector::Stax => Ok(connector_data.stax), Connector::Stripe => Ok(connector_data.stripe), + // Connector::Stripebilling => Ok(connector_data.stripebilling), Connector::Trustpay => Ok(connector_data.trustpay), Connector::Threedsecureio => Ok(connector_data.threedsecureio), Connector::Taxjar => Ok(connector_data.taxjar), diff --git a/crates/hyperswitch_connectors/src/connectors/stripebilling.rs b/crates/hyperswitch_connectors/src/connectors/stripebilling.rs index f3d63ed9ea..f17f1bd302 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripebilling.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripebilling.rs @@ -1,5 +1,7 @@ pub mod transformers; +use std::collections::HashMap; + use common_utils::{ errors::CustomResult, ext_traits::BytesExt, @@ -550,13 +552,89 @@ impl ConnectorIntegration for Stripebil #[async_trait::async_trait] impl webhooks::IncomingWebhook for Stripebilling { + fn get_webhook_source_verification_algorithm( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> + { + Ok(Box::new(common_utils::crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let mut header_hashmap = get_signature_elements_from_header(request.headers)?; + let signature = header_hashmap + .remove("v1") + .ok_or(errors::ConnectorError::WebhookSignatureNotFound)?; + hex::decode(signature).change_context(errors::ConnectorError::WebhookSignatureNotFound) + } + + fn get_webhook_source_verification_message( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _merchant_id: &common_utils::id_type::MerchantId, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let mut header_hashmap = get_signature_elements_from_header(request.headers)?; + let timestamp = header_hashmap + .remove("t") + .ok_or(errors::ConnectorError::WebhookSignatureNotFound)?; + Ok(format!( + "{}.{}", + String::from_utf8_lossy(×tamp), + String::from_utf8_lossy(request.body) + ) + .into_bytes()) + } + + #[cfg(all(feature = "revenue_recovery", feature = "v2"))] + fn get_webhook_object_reference_id( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + // For Stripe billing, we need an additional call to fetch the required recovery data. So, instead of the Invoice ID, we send the Charge ID. + let webhook = + stripebilling::StripebillingWebhookBody::get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId(webhook.data.object.charge), + )) + } + + #[cfg(any(feature = "v1", not(all(feature = "revenue_recovery", feature = "v2"))))] fn get_webhook_object_reference_id( &self, _request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } + #[cfg(all(feature = "revenue_recovery", feature = "v2"))] + fn get_webhook_event_type( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let webhook = + stripebilling::StripebillingWebhookBody::get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + let event = match webhook.event_type { + stripebilling::StripebillingEventType::PaymentSucceeded => { + api_models::webhooks::IncomingWebhookEvent::RecoveryPaymentSuccess + } + stripebilling::StripebillingEventType::PaymentFailed => { + api_models::webhooks::IncomingWebhookEvent::RecoveryPaymentFailure + } + stripebilling::StripebillingEventType::InvoiceDeleted => { + api_models::webhooks::IncomingWebhookEvent::RecoveryInvoiceCancel + } + }; + Ok(event) + } + + #[cfg(any(feature = "v1", not(all(feature = "revenue_recovery", feature = "v2"))))] fn get_webhook_event_type( &self, _request: &webhooks::IncomingWebhookRequestDetails<'_>, @@ -564,12 +642,47 @@ impl webhooks::IncomingWebhook for Stripebilling { Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } + #[cfg(any(feature = "v1", not(all(feature = "revenue_recovery", feature = "v2"))))] fn get_webhook_resource_object( &self, _request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } + + #[cfg(all(feature = "revenue_recovery", feature = "v2"))] + fn get_webhook_resource_object( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let webhook = stripebilling::StripebillingInvoiceBody::get_invoice_webhook_data_from_body( + request.body, + ) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + Ok(Box::new(webhook)) + } +} + +fn get_signature_elements_from_header( + headers: &actix_web::http::header::HeaderMap, +) -> CustomResult>, errors::ConnectorError> { + let security_header = headers + .get("stripe-signature") + .ok_or(errors::ConnectorError::WebhookSignatureNotFound)?; + let security_header_str = security_header + .to_str() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + let header_parts = security_header_str.split(',').collect::>(); + let mut header_hashmap: HashMap> = HashMap::with_capacity(header_parts.len()); + + for header_part in header_parts { + let (header_key, header_value) = header_part + .split_once('=') + .ok_or(errors::ConnectorError::WebhookSignatureNotFound)?; + header_hashmap.insert(header_key.to_string(), header_value.bytes().collect()); + } + + Ok(header_hashmap) } impl ConnectorSpecifications for Stripebilling {} diff --git a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs index dcaa247463..b184f989a4 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "v2")] +use std::str::FromStr; + use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt, types::StringMinorUnit}; +use error_stack::ResultExt; +#[cfg(all(feature = "revenue_recovery", feature = "v2"))] +use hyperswitch_domain_models::revenue_recovery; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, router_data::{ConnectorAuthType, RouterData}, @@ -14,7 +20,7 @@ use serde::{Deserialize, Serialize}; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + utils::{convert_uppercase, PaymentsAuthorizeRequestData}, }; //TODO: Fill the struct with respective fields @@ -94,7 +100,7 @@ impl TryFrom<&ConnectorAuthType> for StripebillingAuthType { } // PaymentsResponse //TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Copy)] #[serde(rename_all = "lowercase")] pub enum StripebillingPaymentStatus { Succeeded, @@ -166,7 +172,7 @@ impl TryFrom<&StripebillingRouterData<&RefundsRouterData>> for Stripebilli // Type definition for Refund Response #[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] +#[derive(Debug, Serialize, Default, Deserialize, Clone, Copy)] pub enum RefundStatus { Succeeded, Failed, @@ -230,3 +236,136 @@ pub struct StripebillingErrorResponse { pub message: String, pub reason: Option, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct StripebillingWebhookBody { + #[serde(rename = "type")] + pub event_type: StripebillingEventType, + pub data: StripebillingWebhookData, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StripebillingInvoiceBody { + #[serde(rename = "type")] + pub event_type: StripebillingEventType, + pub data: StripebillingInvoiceData, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum StripebillingEventType { + #[serde(rename = "invoice.paid")] + PaymentSucceeded, + #[serde(rename = "invoice.payment_failed")] + PaymentFailed, + #[serde(rename = "invoice.voided")] + InvoiceDeleted, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct StripebillingWebhookData { + pub object: StripebillingWebhookObject, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct StripebillingInvoiceData { + pub object: StripebillingWebhookObject, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct StripebillingWebhookObject { + #[serde(rename = "id")] + pub invoice_id: String, + #[serde(deserialize_with = "convert_uppercase")] + pub currency: enums::Currency, + pub customer: String, + #[serde(rename = "amount_remaining")] + pub amount: common_utils::types::MinorUnit, + pub charge: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StripebillingInvoiceObject { + #[serde(rename = "id")] + pub invoice_id: String, + #[serde(deserialize_with = "convert_uppercase")] + pub currency: enums::Currency, + #[serde(rename = "amount_remaining")] + pub amount: common_utils::types::MinorUnit, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StripePaymentMethodDetails { + #[serde(rename = "type")] + pub type_of_payment_method: StripebillingPaymentMethod, + #[serde(rename = "card")] + pub card_funding_type: StripeCardFundingTypeDetails, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum StripebillingPaymentMethod { + Card, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StripeCardFundingTypeDetails { + pub funding: StripebillingFundingTypes, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[serde(rename = "snake_case")] +pub enum StripebillingFundingTypes { + #[serde(rename = "credit")] + Credit, + #[serde(rename = "debit")] + Debit, + #[serde(rename = "prepaid")] + Prepaid, + #[serde(rename = "unknown")] + Unknown, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum StripebillingChargeStatus { + Succeeded, + Failed, + Pending, +} + +impl StripebillingWebhookBody { + pub fn get_webhook_object_from_body(body: &[u8]) -> CustomResult { + let webhook_body: Self = body + .parse_struct::("StripebillingWebhookBody") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + Ok(webhook_body) + } +} + +impl StripebillingInvoiceBody { + pub fn get_invoice_webhook_data_from_body( + body: &[u8], + ) -> CustomResult { + let webhook_body = body + .parse_struct::("StripebillingInvoiceBody") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(webhook_body) + } +} + +#[cfg(all(feature = "revenue_recovery", feature = "v2"))] +impl TryFrom for revenue_recovery::RevenueRecoveryInvoiceData { + type Error = error_stack::Report; + fn try_from(item: StripebillingInvoiceBody) -> Result { + let merchant_reference_id = + common_utils::id_type::PaymentReferenceId::from_str(&item.data.object.invoice_id) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(Self { + amount: item.data.object.amount, + currency: item.data.object.currency, + merchant_reference_id, + }) + } +} diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index fe21b2b72d..a34699498b 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1,4 +1,7 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; use api_models::payments; #[cfg(feature = "payouts")] @@ -58,6 +61,7 @@ use masking::{ExposeInterface, PeekInterface, Secret}; use once_cell::sync::Lazy; use regex::Regex; use router_env::logger; +use serde::Deserialize; use serde_json::Value; use time::PrimitiveDateTime; @@ -5840,3 +5844,14 @@ impl NetworkTokenData for payment_method_data::NetworkTokenData { self.cryptogram.clone() } } + +pub fn convert_uppercase<'de, D, T>(v: D) -> Result +where + D: serde::Deserializer<'de>, + T: FromStr, + ::Err: std::fmt::Debug + std::fmt::Display + std::error::Error, +{ + use serde::de::Error; + let output = <&str>::deserialize(v)?; + output.to_uppercase().parse::().map_err(D::Error::custom) +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index c79154bf68..454a090b63 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1559,6 +1559,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { stripe::transformers::StripeAuthType::try_from(self.auth_type)?; Ok(()) } + // api_enums::Connector::Stripebilling => { + // stripebilling::transformers::StripebillingAuthType::try_from(self.auth_type)?; + // Ok(()) + // } api_enums::Connector::Trustpay => { trustpay::transformers::TrustpayAuthType::try_from(self.auth_type)?; Ok(()) diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 8132d72026..99f87def77 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -538,7 +538,9 @@ impl ConnectorData { enums::Connector::Stripe => { Ok(ConnectorEnum::Old(Box::new(connector::Stripe::new()))) } - // enums::Connector::Stripebilling => Ok(ConnectorEnum::Old(Box::new(connector::Stripebilling))), + // enums::Connector::Stripebilling =>{ + // Ok(ConnectorEnum::Old(Box::new(connector::Stripebilling::new()))) + // }, enums::Connector::Wise => Ok(ConnectorEnum::Old(Box::new(connector::Wise::new()))), enums::Connector::Worldline => { Ok(ConnectorEnum::Old(Box::new(&connector::Worldline)))