feat(connector): [Worldpay] add support for webhook (#820)

This commit is contained in:
Jagan
2023-04-11 13:07:28 +05:30
committed by GitHub
parent b3d1473734
commit 2351116692
7 changed files with 162 additions and 50 deletions

View File

@ -862,10 +862,9 @@ impl api::IncomingWebhook for Nuvei {
_merchant_id: &str,
secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let body: nuvei::NuveiWebhookDetails = request
.query_params_json
.parse_struct("NuveiWebhookDetails")
.switch()?;
let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
let secret_str = std::str::from_utf8(secret)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
@ -887,10 +886,10 @@ impl api::IncomingWebhook for Nuvei {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
let body: nuvei::NuveiWebhookTransactionId = request
.query_params_json
.parse_struct("NuveiWebhookTransactionId")
.switch()?;
let body =
serde_urlencoded::from_str::<nuvei::NuveiWebhookTransactionId>(&request.query_params)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
types::api::PaymentIdType::ConnectorTransactionId(body.ppp_transaction_id),
))
@ -900,10 +899,10 @@ impl api::IncomingWebhook for Nuvei {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
let body: nuvei::NuveiWebhookDataStatus = request
.query_params_json
.parse_struct("NuveiWebhookDataStatus")
.switch()?;
let body =
serde_urlencoded::from_str::<nuvei::NuveiWebhookDataStatus>(&request.query_params)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
match body.status {
nuvei::NuveiWebhookStatus::Approved => {
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
@ -919,10 +918,9 @@ impl api::IncomingWebhook for Nuvei {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
let body: nuvei::NuveiWebhookDetails = request
.query_params_json
.parse_struct("NuveiWebhookDetails")
.switch()?;
let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
let payment_response = nuvei::NuveiPaymentsResponse::from(body);
Encode::<nuvei::NuveiPaymentsResponse>::encode_to_value(&payment_response).switch()
}

View File

@ -486,15 +486,6 @@ impl common_utils::errors::ErrorSwitch<errors::ConnectorError> for errors::Parsi
}
}
pub fn to_string<T>(data: &T) -> Result<String, Error>
where
T: serde::Serialize,
{
serde_json::to_string(data)
.into_report()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
pub fn base64_decode(data: String) -> Result<Vec<u8>, Error> {
consts::BASE64_ENGINE
.decode(data)

View File

@ -4,15 +4,17 @@ mod transformers;
use std::fmt::Debug;
use common_utils::{crypto, ext_traits::ByteSliceExt};
use error_stack::{IntoReport, ResultExt};
use storage_models::enums;
use transformers as worldpay;
use self::{requests::*, response::*};
use super::utils::RefundsRequestData;
use super::utils::{self, RefundsRequestData};
use crate::{
configs::settings,
core::errors::{self, CustomResult},
db::StorageInterface,
headers,
services::{self, ConnectorIntegration},
types::{
@ -20,7 +22,7 @@ use crate::{
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, Response,
},
utils::{self, BytesExt},
utils::{self as ext_traits, BytesExt},
};
#[derive(Debug, Clone)]
@ -384,7 +386,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = WorldpayPaymentsRequest::try_from(req)?;
let worldpay_req =
utils::Encode::<WorldpayPaymentsRequest>::encode_to_string_of_json(&connector_req)
ext_traits::Encode::<WorldpayPaymentsRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(worldpay_req))
}
@ -458,8 +460,9 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
req: &types::RefundExecuteRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = WorldpayRefundRequest::try_from(req)?;
let req = utils::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let req =
ext_traits::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(req))
}
@ -593,24 +596,109 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
#[async_trait::async_trait]
impl api::IncomingWebhook for Worldpay {
fn get_webhook_object_reference_id(
fn get_webhook_source_verification_algorithm(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::Sha256))
}
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let event_signature =
utils::get_header_key_value("Event-Signature", request.headers)?.split(',');
let sign_header = event_signature
.last()
.ok_or_else(|| errors::ConnectorError::WebhookSignatureNotFound)?;
let signature = sign_header
.split('/')
.last()
.ok_or_else(|| errors::ConnectorError::WebhookSignatureNotFound)?;
hex::decode(signature)
.into_report()
.change_context(errors::ConnectorError::WebhookResponseEncodingFailed)
}
async fn get_webhook_source_verification_merchant_secret(
&self,
db: &dyn StorageInterface,
merchant_id: &str,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let key = format!("wh_mer_sec_verification_{}_{}", self.id(), merchant_id);
let secret = db
.get_key(&key)
.await
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
Ok(secret)
}
fn get_webhook_source_verification_message(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let secret_str = std::str::from_utf8(secret)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
let to_sign = format!(
"{}{}",
secret_str,
std::str::from_utf8(request.body)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?
);
Ok(to_sign.into_bytes())
}
fn get_webhook_object_reference_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let body: WorldpayWebhookTransactionId = request
.body
.parse_struct("WorldpayWebhookTransactionId")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
types::api::PaymentIdType::ConnectorTransactionId(
body.event_details.transaction_reference,
),
))
}
fn get_webhook_event_type(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let body: WorldpayWebhookEventType = request
.body
.parse_struct("WorldpayWebhookEventType")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
match body.event_details.event_type {
EventType::SentForSettlement | EventType::Charged => {
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
}
EventType::Error | EventType::Expired => {
Ok(api::IncomingWebhookEvent::PaymentIntentFailure)
}
_ => Err(errors::ConnectorError::WebhookEventTypeNotFound.into()),
}
}
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 body: WorldpayWebhookEventType = request
.body
.parse_struct("WorldpayWebhookEventType")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
let psync_body = WorldpayEventResponse::try_from(body)?;
let res_json = serde_json::to_value(psync_body)
.into_report()
.change_context(errors::ConnectorError::WebhookResponseEncodingFailed)?;
Ok(res_json)
}
}

View File

@ -48,6 +48,8 @@ pub enum EventType {
Refused,
Refunded,
Error,
SentForSettlement,
Expired,
CaptureFailed,
}
@ -305,3 +307,39 @@ pub struct WorldpayErrorResponse {
pub message: String,
pub validation_errors: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldpayWebhookTransactionId {
pub event_details: EventDetails,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventDetails {
pub transaction_reference: String,
#[serde(rename = "type")]
pub event_type: EventType,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldpayWebhookEventType {
pub event_id: String,
pub event_timestamp: String,
pub event_details: EventDetails,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum WorldpayWebhookStatus {
SentForSettlement,
Authorized,
SentForAuthorization,
Cancelled,
Error,
Expired,
Refused,
SentForRefund,
RefundFailed,
}

View File

@ -122,7 +122,7 @@ impl From<EventType> for enums::AttemptStatus {
EventType::Authorized => Self::Authorized,
EventType::CaptureFailed => Self::CaptureFailed,
EventType::Refused => Self::Failure,
EventType::Charged => Self::Charged,
EventType::Charged | EventType::SentForSettlement => Self::Charged,
_ => Self::Pending,
}
}
@ -176,3 +176,13 @@ impl<F> TryFrom<&types::RefundsRouterData<F>> for WorldpayRefundRequest {
})
}
}
impl TryFrom<WorldpayWebhookEventType> for WorldpayEventResponse {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(event: WorldpayWebhookEventType) -> Result<Self, Self::Error> {
Ok(Self {
last_event: event.event_details.event_type,
links: None,
})
}
}

View File

@ -1,8 +1,6 @@
pub mod transformers;
pub mod utils;
use std::collections::HashMap;
use error_stack::{IntoReport, ResultExt};
use masking::ExposeInterface;
use router_env::{instrument, tracing};
@ -488,20 +486,10 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
.attach_printable("Failed construction of ConnectorData")?;
let connector = connector.connector;
let query_params = Some(req.query_string().to_string());
let qp: HashMap<String, String> =
url::form_urlencoded::parse(query_params.unwrap_or_default().as_bytes())
.into_owned()
.collect();
let json = Encode::<HashMap<String, String>>::encode_to_string_of_json(&qp)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("There was an error in parsing the query params")?;
let mut request_details = api::IncomingWebhookRequestDetails {
method: req.method().clone(),
headers: req.headers(),
query_params: req.query_string().to_string(),
query_params_json: json.as_bytes(),
body: &body,
};

View File

@ -17,7 +17,6 @@ pub struct IncomingWebhookRequestDetails<'a> {
pub headers: &'a actix_web::http::header::HeaderMap,
pub body: &'a [u8],
pub query_params: String,
pub query_params_json: &'a [u8],
}
#[async_trait::async_trait]