mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 21:07:58 +08:00
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:
committed by
GitHub
parent
0be1f878ed
commit
3282444132
@ -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),
|
||||
|
||||
@ -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(×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<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 {}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user