feat(connector): [Stripebilling] add incoming webhook support (#7417)

Co-authored-by: Nishanth Challa <nishanth.challa@Nishanth-Challa-C0WGKCFHLF.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
CHALLA NISHANTH BABU
2025-03-17 20:10:10 +05:30
committed by GitHub
parent 0be1f878ed
commit 3282444132
6 changed files with 281 additions and 6 deletions

View File

@ -237,6 +237,7 @@ pub struct ConnectorConfig {
pub stripe: Option<ConnectorTomlConfig>,
#[cfg(feature = "payouts")]
pub stripe_payout: Option<ConnectorTomlConfig>,
// pub stripebilling : Option<ConnectorTomlConfig>,
pub signifyd: Option<ConnectorTomlConfig>,
pub trustpay: Option<ConnectorTomlConfig>,
pub threedsecureio: Option<ConnectorTomlConfig>,
@ -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),

View File

@ -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<RSync, RefundsData, RefundsResponseData> for Stripebil
#[async_trait::async_trait]
impl webhooks::IncomingWebhook for Stripebilling {
fn get_webhook_source_verification_algorithm(
&self,
_request: &webhooks::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn common_utils::crypto::VerifySignature + Send>, 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<Vec<u8>, 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<Vec<u8>, 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(&timestamp),
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<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
// 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<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
}
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
fn get_webhook_event_type(
&self,
request: &webhooks::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::IncomingWebhookEvent, errors::ConnectorError> {
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<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
}
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
fn get_webhook_resource_object(
&self,
request: &webhooks::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, 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<HashMap<String, Vec<u8>>, 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::<Vec<&str>>();
let mut header_hashmap: HashMap<String, Vec<u8>> = 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 {}

View File

@ -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<F> TryFrom<&StripebillingRouterData<&RefundsRouterData<F>>> 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<String>,
}
#[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<Self, errors::ConnectorError> {
let webhook_body: Self = body
.parse_struct::<Self>("StripebillingWebhookBody")
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
Ok(webhook_body)
}
}
impl StripebillingInvoiceBody {
pub fn get_invoice_webhook_data_from_body(
body: &[u8],
) -> CustomResult<Self, errors::ConnectorError> {
let webhook_body = body
.parse_struct::<Self>("StripebillingInvoiceBody")
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
Ok(webhook_body)
}
}
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
impl TryFrom<StripebillingInvoiceBody> for revenue_recovery::RevenueRecoveryInvoiceData {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: StripebillingInvoiceBody) -> Result<Self, Self::Error> {
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,
})
}
}

View File

@ -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<T, D::Error>
where
D: serde::Deserializer<'de>,
T: FromStr,
<T as FromStr>::Err: std::fmt::Debug + std::fmt::Display + std::error::Error,
{
use serde::de::Error;
let output = <&str>::deserialize(v)?;
output.to_uppercase().parse::<T>().map_err(D::Error::custom)
}

View File

@ -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(())

View File

@ -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)))