From eba789640b72cdfbc17d0994d16ce111a1788fe5 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:54:54 +0530 Subject: [PATCH] feat(Connector): [VOLT] Add support for Payments Webhooks (#3155) --- crates/router/src/connector/volt.rs | 87 ++++++- .../router/src/connector/volt/transformers.rs | 223 ++++++++++++++++-- 2 files changed, 279 insertions(+), 31 deletions(-) diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index bf36a7bff6..3641c0c3dd 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -2,11 +2,13 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::request::RequestContent; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; use transformers as volt; +use self::transformers::webhook_headers; +use super::utils; use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -398,7 +400,7 @@ impl ConnectorIntegration CustomResult { - let response: volt::VoltPsyncResponse = res + let response: volt::VoltPaymentsResponseData = res .response .parse_struct("volt PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -586,24 +588,93 @@ impl ConnectorIntegration, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let signature = + utils::get_header_key_value(webhook_headers::X_VOLT_SIGNED, request.headers) + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + + hex::decode(signature) + .into_report() + .change_context(errors::ConnectorError::WebhookVerificationSecretInvalid) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let x_volt_timed = + utils::get_header_key_value(webhook_headers::X_VOLT_TIMED, request.headers)?; + let user_agent = utils::get_header_key_value(webhook_headers::USER_AGENT, request.headers)?; + let version = user_agent + .split('/') + .last() + .ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)?; + Ok(format!( + "{}|{}|{}", + String::from_utf8_lossy(request.body), + x_volt_timed, + version + ) + .into_bytes()) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let webhook_body: volt::VoltWebhookBodyReference = request + .body + .parse_struct("VoltWebhookBodyReference") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + let reference = match webhook_body.merchant_internal_reference { + Some(merchant_internal_reference) => { + api_models::payments::PaymentIdType::PaymentAttemptId(merchant_internal_reference) + } + None => { + api_models::payments::PaymentIdType::ConnectorTransactionId(webhook_body.payment) + } + }; + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + reference, + )) } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + if request.body.is_empty() { + Ok(api::IncomingWebhookEvent::EndpointVerification) + } else { + let payload: volt::VoltWebhookBodyEventType = request + .body + .parse_struct("VoltWebhookBodyEventType") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + Ok(api::IncomingWebhookEvent::from(payload.status)) + } } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let details: volt::VoltWebhookObjectResource = request + .body + .parse_struct("VoltWebhookObjectResource") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + Ok(Box::new(details)) } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index 9ee2a3f012..4c6eaeb52f 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{self, AddressDetailsData, RouterData}, + consts, core::errors, services, types::{self, api, storage::enums as storage_enums}, @@ -41,6 +42,12 @@ impl } } +pub mod webhook_headers { + pub const X_VOLT_SIGNED: &str = "X-Volt-Signed"; + pub const X_VOLT_TIMED: &str = "X-Volt-Timed"; + pub const USER_AGENT: &str = "User-Agent"; +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct VoltPaymentsRequest { @@ -50,7 +57,6 @@ pub struct VoltPaymentsRequest { transaction_type: TransactionType, merchant_internal_reference: String, shopper: ShopperDetails, - notification_url: Option, payment_success_url: Option, payment_failure_url: Option, payment_pending_url: Option, @@ -91,7 +97,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme let payment_failure_url = item.router_data.request.router_return_url.clone(); let payment_pending_url = item.router_data.request.router_return_url.clone(); let payment_cancel_url = item.router_data.request.router_return_url.clone(); - let notification_url = item.router_data.request.webhook_url.clone(); let address = item.router_data.get_billing_address()?; let shopper = ShopperDetails { email: item.router_data.request.email.clone(), @@ -109,7 +114,6 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme payment_failure_url, payment_pending_url, payment_cancel_url, - notification_url, shopper, transaction_type, }) @@ -291,8 +295,9 @@ impl } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Clone, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(strum::Display)] pub enum VoltPaymentStatus { NewPayment, Completed, @@ -309,7 +314,15 @@ pub enum VoltPaymentStatus { Failed, Settled, } -#[derive(Debug, Deserialize)] + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum VoltPaymentsResponseData { + WebhookResponse(VoltWebhookObjectResource), + PsyncResponse(VoltPsyncResponse), +} + +#[derive(Debug, Serialize, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct VoltPsyncResponse { status: VoltPaymentStatus, @@ -317,29 +330,102 @@ pub struct VoltPsyncResponse { merchant_internal_reference: Option, } -impl TryFrom> +impl + TryFrom> for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + item: types::ResponseRouterData< + F, + VoltPaymentsResponseData, + T, + types::PaymentsResponseData, + >, ) -> Result { - Ok(Self { - status: enums::AttemptStatus::from(item.response.status), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: item - .response - .merchant_internal_reference - .or(Some(item.response.id)), - incremental_authorization_allowed: None, - }), - ..item.data - }) + match item.response { + VoltPaymentsResponseData::PsyncResponse(payment_response) => { + let status = enums::AttemptStatus::from(payment_response.status.clone()); + Ok(Self { + status, + response: if is_payment_failure(status) { + Err(types::ErrorResponse { + code: payment_response.status.clone().to_string(), + message: payment_response.status.clone().to_string(), + reason: Some(payment_response.status.to_string()), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(payment_response.id), + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + payment_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: payment_response + .merchant_internal_reference + .or(Some(payment_response.id)), + incremental_authorization_allowed: None, + }) + }, + ..item.data + }) + } + VoltPaymentsResponseData::WebhookResponse(webhook_response) => { + let detailed_status = webhook_response.detailed_status.clone(); + let status = enums::AttemptStatus::from(webhook_response.status); + Ok(Self { + status, + response: if is_payment_failure(status) { + Err(types::ErrorResponse { + code: detailed_status + .clone() + .map(|volt_status| volt_status.to_string()) + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_owned()), + message: detailed_status + .clone() + .map(|volt_status| volt_status.to_string()) + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_owned()), + reason: detailed_status + .clone() + .map(|volt_status| volt_status.to_string()), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(webhook_response.payment.clone()), + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + webhook_response.payment.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: webhook_response + .merchant_internal_reference + .or(Some(webhook_response.payment)), + incremental_authorization_allowed: None, + }) + }, + ..item.data + }) + } + } + } +} + +impl From for enums::AttemptStatus { + fn from(status: VoltWebhookStatus) -> Self { + match status { + VoltWebhookStatus::Completed | VoltWebhookStatus::Received => Self::Charged, + VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => Self::Failure, + VoltWebhookStatus::Pending => Self::Pending, + } } } @@ -405,6 +491,68 @@ impl TryFrom> } } +#[derive(Debug, Deserialize, Clone, Serialize)] +pub struct VoltWebhookBodyReference { + pub payment: String, + pub merchant_internal_reference: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltWebhookBodyEventType { + pub status: VoltWebhookStatus, + pub detailed_status: Option, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltWebhookObjectResource { + pub payment: String, + pub merchant_internal_reference: Option, + pub status: VoltWebhookStatus, + pub detailed_status: Option, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VoltWebhookStatus { + Completed, + Failed, + Pending, + Received, + NotReceived, +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(strum::Display)] +pub enum VoltDetailedStatus { + RefusedByRisk, + RefusedByBank, + ErrorAtBank, + CancelledByUser, + AbandonedByUser, + Failed, + Completed, + BankRedirect, + DelayedAtBank, + AwaitingCheckoutAuthorisation, +} + +impl From for api::IncomingWebhookEvent { + fn from(status: VoltWebhookStatus) -> Self { + match status { + VoltWebhookStatus::Completed | VoltWebhookStatus::Received => { + Self::PaymentIntentSuccess + } + VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => { + Self::PaymentIntentFailure + } + VoltWebhookStatus::Pending => Self::PaymentIntentProcessing, + } + } +} + #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] pub struct VoltErrorResponse { pub exception: VoltErrorException, @@ -429,3 +577,32 @@ pub struct VoltErrorList { pub property: String, pub message: String, } + +fn is_payment_failure(status: enums::AttemptStatus) -> bool { + match status { + common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::Failure => true, + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::RouterDeclined + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => false, + } +}