Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(connector): [Bluesnap] Update incoming Webhooks flow #1982

Merged
merged 7 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 38 additions & 56 deletions crates/router/src/connector/bluesnap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ use crate::{
errors::{self, CustomResult},
payments,
},
db::StorageInterface,
headers, logger,
services::{
self,
Expand All @@ -33,7 +32,7 @@ use crate::{
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
domain, ErrorResponse, Response,
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
Expand Down Expand Up @@ -975,19 +974,17 @@ impl api::IncomingWebhook for Bluesnap {
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::Md5))
Ok(Box::new(crypto::HmacSha256))
}

fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let webhook_body: bluesnap::BluesnapWebhookBody =
serde_urlencoded::from_bytes(request.body)
.into_report()
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
let signature = webhook_body.auth_key;
hex::decode(signature)
let security_header =
connector_utils::get_header_key_value("bls-signature", request.headers)?;

hex::decode(security_header)
.into_report()
.change_context(errors::ConnectorError::WebhookSignatureNotFound)
}
Expand All @@ -997,51 +994,10 @@ impl api::IncomingWebhook for Bluesnap {
_merchant_id: &str,
_secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let webhook_body: bluesnap::BluesnapWebhookBody =
serde_urlencoded::from_bytes(request.body)
.into_report()
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
let msg = webhook_body.reference_number + webhook_body.contract_id.as_str();
Ok(msg.into_bytes())
}
let timestamp =
connector_utils::get_header_key_value("bls-ipn-timestamp", request.headers)?;

async fn verify_webhook_source(
&self,
db: &dyn StorageInterface,
request: &api::IncomingWebhookRequestDetails<'_>,
merchant_account: &domain::MerchantAccount,
connector_label: &str,
key_store: &domain::MerchantKeyStore,
object_reference_id: api_models::webhooks::ObjectReferenceId,
) -> CustomResult<bool, errors::ConnectorError> {
let algorithm = self
.get_webhook_source_verification_algorithm(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

let signature = self
.get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let mut secret = self
.get_webhook_source_verification_merchant_secret(
db,
merchant_account,
connector_label,
key_store,
object_reference_id,
)
.await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let mut message = self
.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&secret,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
message.append(&mut secret);
algorithm
.verify_signature(&secret, &signature, &message)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
Ok(format!("{}{}", timestamp, String::from_utf8_lossy(request.body)).into_bytes())
}

fn get_webhook_object_reference_id(
Expand All @@ -1053,8 +1009,8 @@ impl api::IncomingWebhook for Bluesnap {
.into_report()
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(
webhook_body.reference_number,
api_models::payments::PaymentIdType::PaymentAttemptId(
webhook_body.merchant_transaction_id,
),
))
}
Expand Down Expand Up @@ -1090,8 +1046,34 @@ impl api::IncomingWebhook for Bluesnap {
serde_urlencoded::from_bytes(request.body)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;

let (card_transaction_type, processing_status) = match details.transaction_type {
bluesnap::BluesnapWebhookEvents::Decline
| bluesnap::BluesnapWebhookEvents::CcChargeFailed => Ok((
bluesnap::BluesnapTxnType::Capture,
bluesnap::BluesnapProcessingStatus::Fail,
)),
bluesnap::BluesnapWebhookEvents::Charge => Ok((
bluesnap::BluesnapTxnType::Capture,
bluesnap::BluesnapProcessingStatus::Success,
)),
bluesnap::BluesnapWebhookEvents::Unknown => {
Err(errors::ConnectorError::WebhookEventTypeNotFound)
}
}?;

let psync_struct = bluesnap::BluesnapPaymentsResponse {
processing_info: bluesnap::ProcessingInfoResponse {
processing_status,
authorization_code: None,
network_transaction_id: None,
},
transaction_id: details.reference_number,
card_transaction_type,
};
SamraatBansal marked this conversation as resolved.
Show resolved Hide resolved

let res_json =
utils::Encode::<transformers::BluesnapWebhookObjectResource>::encode_to_value(&details)
utils::Encode::<transformers::BluesnapPaymentsResponse>::encode_to_value(&psync_struct)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;

Ok(res_json)
Expand Down
23 changes: 12 additions & 11 deletions crates/router/src/connector/bluesnap/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct BluesnapPaymentsRequest {
three_d_secure: Option<BluesnapThreeDSecureInfo>,
transaction_fraud_info: Option<TransactionFraudInfo>,
card_holder_info: Option<BluesnapCardHolderInfo>,
merchant_transaction_id: Option<String>,
}

#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -318,6 +319,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest {
fraud_session_id: item.payment_id.clone(),
}),
card_holder_info,
merchant_transaction_id: Some(item.connector_request_reference_id.clone()),
})
}
}
Expand Down Expand Up @@ -485,6 +487,7 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRe
item.get_billing_address()?,
item.request.get_email()?,
)?,
merchant_transaction_id: Some(item.connector_request_reference_id.clone()),
})
}
}
Expand Down Expand Up @@ -679,9 +682,9 @@ impl From<BluesnapProcessingStatus> for enums::RefundStatus {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapPaymentsResponse {
processing_info: ProcessingInfoResponse,
transaction_id: String,
card_transaction_type: BluesnapTxnType,
pub processing_info: ProcessingInfoResponse,
pub transaction_id: String,
pub card_transaction_type: BluesnapTxnType,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -701,9 +704,9 @@ pub struct Refund {
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessingInfoResponse {
processing_status: BluesnapProcessingStatus,
authorization_code: Option<String>,
network_transaction_id: Option<Secret<String>>,
pub processing_status: BluesnapProcessingStatus,
pub authorization_code: Option<String>,
pub network_transaction_id: Option<Secret<String>>,
}

impl<F, T>
Expand Down Expand Up @@ -802,8 +805,7 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapWebhookBody {
pub auth_key: String,
pub contract_id: String,
pub merchant_transaction_id: String,
pub reference_number: String,
}

Expand All @@ -822,12 +824,11 @@ pub enum BluesnapWebhookEvents {
#[serde(other)]
Unknown,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BluesnapWebhookObjectResource {
pub auth_key: String,
pub contract_id: String,
pub reference_number: String,
pub transaction_type: BluesnapWebhookEvents,
}

#[derive(Default, Debug, Clone, Serialize, Deserialize)]
Expand Down
10 changes: 5 additions & 5 deletions crates/router/tests/connectors/bluesnap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ async fn should_fail_payment_for_incorrect_cvc() {
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Order creation failure due to problematic input.".to_string(),
"VALIDATION_GENERAL_FAILURE".to_string(),
);
}

Expand All @@ -437,7 +437,7 @@ async fn should_fail_payment_for_invalid_exp_month() {
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Order creation failure due to problematic input.".to_string(),
"VALIDATION_GENERAL_FAILURE".to_string(),
);
}

Expand All @@ -463,7 +463,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() {
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Order creation failure due to problematic input.".to_string(),
"VALIDATION_GENERAL_FAILURE".to_string(),
);
}

Expand All @@ -485,7 +485,7 @@ async fn should_fail_void_payment_for_auto_capture() {
.unwrap();
assert_eq!(
void_response.response.unwrap_err().message,
"Transaction AUTH_REVERSAL failed. Transaction has already been captured."
"TRANSACTION_ALREADY_CAPTURED"
);
}

Expand Down Expand Up @@ -524,7 +524,7 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() {
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Refund amount cannot be more than the refundable order amount.",
"REFUND_MAX_AMOUNT_FAILURE",
);
}

Expand Down
Loading