Skip to content

Commit

Permalink
feat(connector): [Braintree] implement dispute webhook (#2031)
Browse files Browse the repository at this point in the history
Co-authored-by: Prasunna Soppa <[email protected]>
Co-authored-by: Arjun Karthik <[email protected]>
Co-authored-by: Sai Harsha Vardhan <[email protected]>
  • Loading branch information
4 people authored Oct 6, 2023
1 parent 1d57677 commit eeccd10
Show file tree
Hide file tree
Showing 26 changed files with 375 additions and 63 deletions.
2 changes: 2 additions & 0 deletions crates/router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] }
uuid = { version = "1.3.3", features = ["serde", "v4"] }
openssl = "0.10.55"
x509-parser = "0.15.0"
sha-1 = { version = "0.9"}
digest = "0.9"

# First party crates
api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] }
Expand Down
13 changes: 8 additions & 5 deletions crates/router/src/connector/adyen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,7 @@ impl api::IncomingWebhook for Adyen {
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let notif_item = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
Expand All @@ -1448,7 +1449,7 @@ impl api::IncomingWebhook for Adyen {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
_secret: &[u8],
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
Expand All @@ -1475,9 +1476,6 @@ impl api::IncomingWebhook for Adyen {
merchant_connector_account: domain::MerchantConnectorAccount,
connector_label: &str,
) -> CustomResult<bool, errors::ConnectorError> {
let signature = self
.get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
merchant_account,
Expand All @@ -1486,11 +1484,16 @@ impl api::IncomingWebhook for Adyen {
)
.await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

let signature = self
.get_webhook_source_verification_signature(request, &connector_webhook_secrets)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

let message = self
.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&connector_webhook_secrets.secret,
&connector_webhook_secrets,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

Expand Down
3 changes: 2 additions & 1 deletion crates/router/src/connector/airwallex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,7 @@ impl api::IncomingWebhook for Airwallex {
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let security_header = request
.headers
Expand All @@ -969,7 +970,7 @@ impl api::IncomingWebhook for Airwallex {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
_secret: &[u8],
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let timestamp = request
.headers
Expand Down
3 changes: 2 additions & 1 deletion crates/router/src/connector/authorizedotnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ impl api::IncomingWebhook for Authorizedotnet {
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let security_header = request
.headers
Expand Down Expand Up @@ -772,7 +773,7 @@ impl api::IncomingWebhook for Authorizedotnet {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
_secret: &[u8],
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
Ok(request.body.to_vec())
}
Expand Down
4 changes: 2 additions & 2 deletions crates/router/src/connector/bluesnap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,7 @@ impl api::IncomingWebhook for Bluesnap {
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let security_header =
connector_utils::get_header_key_value("bls-signature", request.headers)?;
Expand All @@ -996,11 +997,10 @@ impl api::IncomingWebhook for Bluesnap {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
_secret: &[u8],
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let timestamp =
connector_utils::get_header_key_value("bls-ipn-timestamp", request.headers)?;

Ok(format!("{}{}", timestamp, String::from_utf8_lossy(request.body)).into_bytes())
}

Expand Down
224 changes: 215 additions & 9 deletions crates/router/src/connector/braintree.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
pub mod braintree_graphql_transformers;
pub mod transformers;
use std::{fmt::Debug, str::FromStr};

use std::fmt::Debug;

use api_models::webhooks::IncomingWebhookEvent;
use base64::Engine;
use common_utils::{crypto, ext_traits::XmlExt};
use diesel_models::enums;
use error_stack::{IntoReport, Report, ResultExt};
use masking::PeekInterface;
use masking::{ExposeInterface, PeekInterface};
use ring::hmac;
use sha1::{Digest, Sha1};

use self::transformers as braintree;
use super::utils::PaymentsAuthorizeRequestData;
Expand All @@ -26,6 +30,8 @@ use crate::{
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
domain,
transformers::ForeignFrom,
ErrorResponse,
},
utils::{self, BytesExt},
Expand Down Expand Up @@ -1264,28 +1270,228 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse

#[async_trait::async_trait]
impl api::IncomingWebhook for Braintree {
fn get_webhook_source_verification_algorithm(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::HmacSha1))
}

fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let notif_item = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

let signature_pairs: Vec<(&str, &str)> = notif_item
.bt_signature
.split('&')
.collect::<Vec<&str>>()
.into_iter()
.map(|pair| pair.split_once('|').unwrap_or(("", "")))
.collect::<Vec<(_, _)>>();

let merchant_secret = connector_webhook_secrets
.additional_secret //public key
.clone()
.ok_or(errors::ConnectorError::WebhookVerificationSecretNotFound)?;

let signature = get_matching_webhook_signature(signature_pairs, merchant_secret.expose())
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?;
Ok(signature.as_bytes().to_vec())
}

fn get_webhook_source_verification_message(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let notify = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

let message = notify.bt_payload.to_string();

Ok(message.into_bytes())
}

async fn verify_webhook_source(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
merchant_account: &domain::MerchantAccount,
merchant_connector_account: domain::MerchantConnectorAccount,
connector_label: &str,
) -> CustomResult<bool, errors::ConnectorError> {
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
merchant_account,
connector_label,
merchant_connector_account,
)
.await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

let signature = self
.get_webhook_source_verification_signature(request, &connector_webhook_secrets)
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;

let message = self
.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&connector_webhook_secrets,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

let sha1_hash_key = Sha1::digest(&connector_webhook_secrets.secret);

let signing_key = hmac::Key::new(
hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
sha1_hash_key.as_slice(),
);
let signed_messaged = hmac::sign(&signing_key, &message);
let payload_sign: String = hex::encode(signed_messaged);
Ok(payload_sign.as_bytes().eq(&signature))
}

fn get_webhook_object_reference_id(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let notif = get_webhook_object_from_body(_request.body)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;

let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;

match response.dispute {
Some(dispute_data) => Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(
dispute_data.transaction.id,
),
)),
None => Err(errors::ConnectorError::WebhookReferenceIdNotFound).into_report(),
}
}

fn get_webhook_event_type(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Ok(api::IncomingWebhookEvent::EventNotSupported)
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;

let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;

Ok(IncomingWebhookEvent::foreign_from(response.kind.as_str()))
}

fn get_webhook_resource_object(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;

let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;

let res_json = serde_json::to_value(response)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;

Ok(res_json)
}

fn get_webhook_api_response(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<services::api::ApplicationResponse<serde_json::Value>, errors::ConnectorError>
{
Ok(services::api::ApplicationResponse::TextPlain(
"[accepted]".to_string(),
))
}

fn get_dispute_details(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::disputes::DisputePayload, errors::ConnectorError> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;

let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;

match response.dispute {
Some(dispute_data) => {
let currency = diesel_models::enums::Currency::from_str(
dispute_data.currency_iso_code.as_str(),
)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
Ok(api::disputes::DisputePayload {
amount: connector_utils::to_currency_lower_unit(
dispute_data.amount_disputed.to_string(),
currency,
)?,
currency: dispute_data.currency_iso_code,
dispute_stage: braintree_graphql_transformers::get_dispute_stage(
dispute_data.kind.as_str(),
)?,
connector_dispute_id: dispute_data.id,
connector_reason: dispute_data.reason,
connector_reason_code: dispute_data.reason_code,
challenge_required_by: dispute_data.reply_by_date,
connector_status: dispute_data.status,
created_at: dispute_data.created_at,
updated_at: dispute_data.updated_at,
})
}
None => Err(errors::ConnectorError::WebhookResourceObjectNotFound)?,
}
}
}

fn get_matching_webhook_signature(
signature_pairs: Vec<(&str, &str)>,
secret: String,
) -> Option<String> {
for (public_key, signature) in signature_pairs {
if *public_key == secret {
return Some(signature.to_string());
}
}
None
}

fn get_webhook_object_from_body(
body: &[u8],
) -> CustomResult<braintree_graphql_transformers::BraintreeWebhookResponse, errors::ParsingError> {
serde_urlencoded::from_bytes::<braintree_graphql_transformers::BraintreeWebhookResponse>(body)
.into_report()
.change_context(errors::ParsingError::StructParseFailure(
"BraintreeWebhookResponse",
))
}

fn decode_webhook_payload(
payload: &[u8],
) -> CustomResult<braintree_graphql_transformers::Notification, errors::ConnectorError> {
let decoded_response = consts::BASE64_ENGINE
.decode(payload)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;

let xml_response = String::from_utf8(decoded_response)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;

xml_response
.parse_xml::<braintree_graphql_transformers::Notification>()
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
}

impl services::ConnectorRedirectResponse for Braintree {
fn get_flow_type(
&self,
Expand Down
Loading

0 comments on commit eeccd10

Please sign in to comment.