Skip to content

Commit eeccd10

Browse files
AkshayaFoigerprasunna09ArjunKarthiksai-harsha-vardhan
authored
feat(connector): [Braintree] implement dispute webhook (#2031)
Co-authored-by: Prasunna Soppa <[email protected]> Co-authored-by: Arjun Karthik <[email protected]> Co-authored-by: Sai Harsha Vardhan <[email protected]>
1 parent 1d57677 commit eeccd10

26 files changed

+375
-63
lines changed

crates/router/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] }
8787
uuid = { version = "1.3.3", features = ["serde", "v4"] }
8888
openssl = "0.10.55"
8989
x509-parser = "0.15.0"
90+
sha-1 = { version = "0.9"}
91+
digest = "0.9"
9092

9193
# First party crates
9294
api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] }

crates/router/src/connector/adyen.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,7 @@ impl api::IncomingWebhook for Adyen {
14361436
fn get_webhook_source_verification_signature(
14371437
&self,
14381438
request: &api::IncomingWebhookRequestDetails<'_>,
1439+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
14391440
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
14401441
let notif_item = get_webhook_object_from_body(request.body)
14411442
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
@@ -1448,7 +1449,7 @@ impl api::IncomingWebhook for Adyen {
14481449
&self,
14491450
request: &api::IncomingWebhookRequestDetails<'_>,
14501451
_merchant_id: &str,
1451-
_secret: &[u8],
1452+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
14521453
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
14531454
let notif = get_webhook_object_from_body(request.body)
14541455
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
@@ -1475,9 +1476,6 @@ impl api::IncomingWebhook for Adyen {
14751476
merchant_connector_account: domain::MerchantConnectorAccount,
14761477
connector_label: &str,
14771478
) -> CustomResult<bool, errors::ConnectorError> {
1478-
let signature = self
1479-
.get_webhook_source_verification_signature(request)
1480-
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
14811479
let connector_webhook_secrets = self
14821480
.get_webhook_source_verification_merchant_secret(
14831481
merchant_account,
@@ -1486,11 +1484,16 @@ impl api::IncomingWebhook for Adyen {
14861484
)
14871485
.await
14881486
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1487+
1488+
let signature = self
1489+
.get_webhook_source_verification_signature(request, &connector_webhook_secrets)
1490+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1491+
14891492
let message = self
14901493
.get_webhook_source_verification_message(
14911494
request,
14921495
&merchant_account.merchant_id,
1493-
&connector_webhook_secrets.secret,
1496+
&connector_webhook_secrets,
14941497
)
14951498
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
14961499

crates/router/src/connector/airwallex.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ impl api::IncomingWebhook for Airwallex {
946946
fn get_webhook_source_verification_signature(
947947
&self,
948948
request: &api::IncomingWebhookRequestDetails<'_>,
949+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
949950
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
950951
let security_header = request
951952
.headers
@@ -969,7 +970,7 @@ impl api::IncomingWebhook for Airwallex {
969970
&self,
970971
request: &api::IncomingWebhookRequestDetails<'_>,
971972
_merchant_id: &str,
972-
_secret: &[u8],
973+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
973974
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
974975
let timestamp = request
975976
.headers

crates/router/src/connector/authorizedotnet.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ impl api::IncomingWebhook for Authorizedotnet {
745745
fn get_webhook_source_verification_signature(
746746
&self,
747747
request: &api::IncomingWebhookRequestDetails<'_>,
748+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
748749
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
749750
let security_header = request
750751
.headers
@@ -772,7 +773,7 @@ impl api::IncomingWebhook for Authorizedotnet {
772773
&self,
773774
request: &api::IncomingWebhookRequestDetails<'_>,
774775
_merchant_id: &str,
775-
_secret: &[u8],
776+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
776777
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
777778
Ok(request.body.to_vec())
778779
}

crates/router/src/connector/bluesnap.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,7 @@ impl api::IncomingWebhook for Bluesnap {
984984
fn get_webhook_source_verification_signature(
985985
&self,
986986
request: &api::IncomingWebhookRequestDetails<'_>,
987+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
987988
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
988989
let security_header =
989990
connector_utils::get_header_key_value("bls-signature", request.headers)?;
@@ -996,11 +997,10 @@ impl api::IncomingWebhook for Bluesnap {
996997
&self,
997998
request: &api::IncomingWebhookRequestDetails<'_>,
998999
_merchant_id: &str,
999-
_secret: &[u8],
1000+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
10001001
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
10011002
let timestamp =
10021003
connector_utils::get_header_key_value("bls-ipn-timestamp", request.headers)?;
1003-
10041004
Ok(format!("{}{}", timestamp, String::from_utf8_lossy(request.body)).into_bytes())
10051005
}
10061006

crates/router/src/connector/braintree.rs

Lines changed: 215 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
pub mod braintree_graphql_transformers;
22
pub mod transformers;
3+
use std::{fmt::Debug, str::FromStr};
34

4-
use std::fmt::Debug;
5-
5+
use api_models::webhooks::IncomingWebhookEvent;
6+
use base64::Engine;
7+
use common_utils::{crypto, ext_traits::XmlExt};
68
use diesel_models::enums;
79
use error_stack::{IntoReport, Report, ResultExt};
8-
use masking::PeekInterface;
10+
use masking::{ExposeInterface, PeekInterface};
11+
use ring::hmac;
12+
use sha1::{Digest, Sha1};
913

1014
use self::transformers as braintree;
1115
use super::utils::PaymentsAuthorizeRequestData;
@@ -26,6 +30,8 @@ use crate::{
2630
types::{
2731
self,
2832
api::{self, ConnectorCommon, ConnectorCommonExt},
33+
domain,
34+
transformers::ForeignFrom,
2935
ErrorResponse,
3036
},
3137
utils::{self, BytesExt},
@@ -1264,28 +1270,228 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
12641270

12651271
#[async_trait::async_trait]
12661272
impl api::IncomingWebhook for Braintree {
1273+
fn get_webhook_source_verification_algorithm(
1274+
&self,
1275+
_request: &api::IncomingWebhookRequestDetails<'_>,
1276+
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
1277+
Ok(Box::new(crypto::HmacSha1))
1278+
}
1279+
1280+
fn get_webhook_source_verification_signature(
1281+
&self,
1282+
request: &api::IncomingWebhookRequestDetails<'_>,
1283+
connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
1284+
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
1285+
let notif_item = get_webhook_object_from_body(request.body)
1286+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1287+
1288+
let signature_pairs: Vec<(&str, &str)> = notif_item
1289+
.bt_signature
1290+
.split('&')
1291+
.collect::<Vec<&str>>()
1292+
.into_iter()
1293+
.map(|pair| pair.split_once('|').unwrap_or(("", "")))
1294+
.collect::<Vec<(_, _)>>();
1295+
1296+
let merchant_secret = connector_webhook_secrets
1297+
.additional_secret //public key
1298+
.clone()
1299+
.ok_or(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
1300+
1301+
let signature = get_matching_webhook_signature(signature_pairs, merchant_secret.expose())
1302+
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)?;
1303+
Ok(signature.as_bytes().to_vec())
1304+
}
1305+
1306+
fn get_webhook_source_verification_message(
1307+
&self,
1308+
request: &api::IncomingWebhookRequestDetails<'_>,
1309+
_merchant_id: &str,
1310+
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
1311+
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
1312+
let notify = get_webhook_object_from_body(request.body)
1313+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1314+
1315+
let message = notify.bt_payload.to_string();
1316+
1317+
Ok(message.into_bytes())
1318+
}
1319+
1320+
async fn verify_webhook_source(
1321+
&self,
1322+
request: &api::IncomingWebhookRequestDetails<'_>,
1323+
merchant_account: &domain::MerchantAccount,
1324+
merchant_connector_account: domain::MerchantConnectorAccount,
1325+
connector_label: &str,
1326+
) -> CustomResult<bool, errors::ConnectorError> {
1327+
let connector_webhook_secrets = self
1328+
.get_webhook_source_verification_merchant_secret(
1329+
merchant_account,
1330+
connector_label,
1331+
merchant_connector_account,
1332+
)
1333+
.await
1334+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1335+
1336+
let signature = self
1337+
.get_webhook_source_verification_signature(request, &connector_webhook_secrets)
1338+
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
1339+
1340+
let message = self
1341+
.get_webhook_source_verification_message(
1342+
request,
1343+
&merchant_account.merchant_id,
1344+
&connector_webhook_secrets,
1345+
)
1346+
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
1347+
1348+
let sha1_hash_key = Sha1::digest(&connector_webhook_secrets.secret);
1349+
1350+
let signing_key = hmac::Key::new(
1351+
hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
1352+
sha1_hash_key.as_slice(),
1353+
);
1354+
let signed_messaged = hmac::sign(&signing_key, &message);
1355+
let payload_sign: String = hex::encode(signed_messaged);
1356+
Ok(payload_sign.as_bytes().eq(&signature))
1357+
}
1358+
12671359
fn get_webhook_object_reference_id(
12681360
&self,
12691361
_request: &api::IncomingWebhookRequestDetails<'_>,
12701362
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
1271-
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
1363+
let notif = get_webhook_object_from_body(_request.body)
1364+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1365+
1366+
let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;
1367+
1368+
match response.dispute {
1369+
Some(dispute_data) => Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
1370+
api_models::payments::PaymentIdType::ConnectorTransactionId(
1371+
dispute_data.transaction.id,
1372+
),
1373+
)),
1374+
None => Err(errors::ConnectorError::WebhookReferenceIdNotFound).into_report(),
1375+
}
12721376
}
12731377

12741378
fn get_webhook_event_type(
12751379
&self,
1276-
_request: &api::IncomingWebhookRequestDetails<'_>,
1277-
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
1278-
Ok(api::IncomingWebhookEvent::EventNotSupported)
1380+
request: &api::IncomingWebhookRequestDetails<'_>,
1381+
) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
1382+
let notif = get_webhook_object_from_body(request.body)
1383+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1384+
1385+
let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;
1386+
1387+
Ok(IncomingWebhookEvent::foreign_from(response.kind.as_str()))
12791388
}
12801389

12811390
fn get_webhook_resource_object(
12821391
&self,
1283-
_request: &api::IncomingWebhookRequestDetails<'_>,
1392+
request: &api::IncomingWebhookRequestDetails<'_>,
12841393
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
1285-
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
1394+
let notif = get_webhook_object_from_body(request.body)
1395+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1396+
1397+
let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;
1398+
1399+
let res_json = serde_json::to_value(response)
1400+
.into_report()
1401+
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
1402+
1403+
Ok(res_json)
1404+
}
1405+
1406+
fn get_webhook_api_response(
1407+
&self,
1408+
_request: &api::IncomingWebhookRequestDetails<'_>,
1409+
) -> CustomResult<services::api::ApplicationResponse<serde_json::Value>, errors::ConnectorError>
1410+
{
1411+
Ok(services::api::ApplicationResponse::TextPlain(
1412+
"[accepted]".to_string(),
1413+
))
1414+
}
1415+
1416+
fn get_dispute_details(
1417+
&self,
1418+
request: &api::IncomingWebhookRequestDetails<'_>,
1419+
) -> CustomResult<api::disputes::DisputePayload, errors::ConnectorError> {
1420+
let notif = get_webhook_object_from_body(request.body)
1421+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1422+
1423+
let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?;
1424+
1425+
match response.dispute {
1426+
Some(dispute_data) => {
1427+
let currency = diesel_models::enums::Currency::from_str(
1428+
dispute_data.currency_iso_code.as_str(),
1429+
)
1430+
.into_report()
1431+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1432+
Ok(api::disputes::DisputePayload {
1433+
amount: connector_utils::to_currency_lower_unit(
1434+
dispute_data.amount_disputed.to_string(),
1435+
currency,
1436+
)?,
1437+
currency: dispute_data.currency_iso_code,
1438+
dispute_stage: braintree_graphql_transformers::get_dispute_stage(
1439+
dispute_data.kind.as_str(),
1440+
)?,
1441+
connector_dispute_id: dispute_data.id,
1442+
connector_reason: dispute_data.reason,
1443+
connector_reason_code: dispute_data.reason_code,
1444+
challenge_required_by: dispute_data.reply_by_date,
1445+
connector_status: dispute_data.status,
1446+
created_at: dispute_data.created_at,
1447+
updated_at: dispute_data.updated_at,
1448+
})
1449+
}
1450+
None => Err(errors::ConnectorError::WebhookResourceObjectNotFound)?,
1451+
}
12861452
}
12871453
}
12881454

1455+
fn get_matching_webhook_signature(
1456+
signature_pairs: Vec<(&str, &str)>,
1457+
secret: String,
1458+
) -> Option<String> {
1459+
for (public_key, signature) in signature_pairs {
1460+
if *public_key == secret {
1461+
return Some(signature.to_string());
1462+
}
1463+
}
1464+
None
1465+
}
1466+
1467+
fn get_webhook_object_from_body(
1468+
body: &[u8],
1469+
) -> CustomResult<braintree_graphql_transformers::BraintreeWebhookResponse, errors::ParsingError> {
1470+
serde_urlencoded::from_bytes::<braintree_graphql_transformers::BraintreeWebhookResponse>(body)
1471+
.into_report()
1472+
.change_context(errors::ParsingError::StructParseFailure(
1473+
"BraintreeWebhookResponse",
1474+
))
1475+
}
1476+
1477+
fn decode_webhook_payload(
1478+
payload: &[u8],
1479+
) -> CustomResult<braintree_graphql_transformers::Notification, errors::ConnectorError> {
1480+
let decoded_response = consts::BASE64_ENGINE
1481+
.decode(payload)
1482+
.into_report()
1483+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1484+
1485+
let xml_response = String::from_utf8(decoded_response)
1486+
.into_report()
1487+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1488+
1489+
xml_response
1490+
.parse_xml::<braintree_graphql_transformers::Notification>()
1491+
.into_report()
1492+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
1493+
}
1494+
12891495
impl services::ConnectorRedirectResponse for Braintree {
12901496
fn get_flow_type(
12911497
&self,

0 commit comments

Comments
 (0)