mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 17:47:54 +08:00
feat(connector): [Worldpay] add support for webhook (#820)
This commit is contained in:
@ -862,10 +862,9 @@ impl api::IncomingWebhook for Nuvei {
|
|||||||
_merchant_id: &str,
|
_merchant_id: &str,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
let body: nuvei::NuveiWebhookDetails = request
|
let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params)
|
||||||
.query_params_json
|
.into_report()
|
||||||
.parse_struct("NuveiWebhookDetails")
|
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||||
.switch()?;
|
|
||||||
let secret_str = std::str::from_utf8(secret)
|
let secret_str = std::str::from_utf8(secret)
|
||||||
.into_report()
|
.into_report()
|
||||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||||
@ -887,10 +886,10 @@ impl api::IncomingWebhook for Nuvei {
|
|||||||
&self,
|
&self,
|
||||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||||
let body: nuvei::NuveiWebhookTransactionId = request
|
let body =
|
||||||
.query_params_json
|
serde_urlencoded::from_str::<nuvei::NuveiWebhookTransactionId>(&request.query_params)
|
||||||
.parse_struct("NuveiWebhookTransactionId")
|
.into_report()
|
||||||
.switch()?;
|
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||||
types::api::PaymentIdType::ConnectorTransactionId(body.ppp_transaction_id),
|
types::api::PaymentIdType::ConnectorTransactionId(body.ppp_transaction_id),
|
||||||
))
|
))
|
||||||
@ -900,10 +899,10 @@ impl api::IncomingWebhook for Nuvei {
|
|||||||
&self,
|
&self,
|
||||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||||
let body: nuvei::NuveiWebhookDataStatus = request
|
let body =
|
||||||
.query_params_json
|
serde_urlencoded::from_str::<nuvei::NuveiWebhookDataStatus>(&request.query_params)
|
||||||
.parse_struct("NuveiWebhookDataStatus")
|
.into_report()
|
||||||
.switch()?;
|
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||||
match body.status {
|
match body.status {
|
||||||
nuvei::NuveiWebhookStatus::Approved => {
|
nuvei::NuveiWebhookStatus::Approved => {
|
||||||
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
|
Ok(api::IncomingWebhookEvent::PaymentIntentSuccess)
|
||||||
@ -919,10 +918,9 @@ impl api::IncomingWebhook for Nuvei {
|
|||||||
&self,
|
&self,
|
||||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||||
let body: nuvei::NuveiWebhookDetails = request
|
let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params)
|
||||||
.query_params_json
|
.into_report()
|
||||||
.parse_struct("NuveiWebhookDetails")
|
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||||
.switch()?;
|
|
||||||
let payment_response = nuvei::NuveiPaymentsResponse::from(body);
|
let payment_response = nuvei::NuveiPaymentsResponse::from(body);
|
||||||
Encode::<nuvei::NuveiPaymentsResponse>::encode_to_value(&payment_response).switch()
|
Encode::<nuvei::NuveiPaymentsResponse>::encode_to_value(&payment_response).switch()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> {
|
pub fn base64_decode(data: String) -> Result<Vec<u8>, Error> {
|
||||||
consts::BASE64_ENGINE
|
consts::BASE64_ENGINE
|
||||||
.decode(data)
|
.decode(data)
|
||||||
|
|||||||
@ -4,15 +4,17 @@ mod transformers;
|
|||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use common_utils::{crypto, ext_traits::ByteSliceExt};
|
||||||
use error_stack::{IntoReport, ResultExt};
|
use error_stack::{IntoReport, ResultExt};
|
||||||
use storage_models::enums;
|
use storage_models::enums;
|
||||||
use transformers as worldpay;
|
use transformers as worldpay;
|
||||||
|
|
||||||
use self::{requests::*, response::*};
|
use self::{requests::*, response::*};
|
||||||
use super::utils::RefundsRequestData;
|
use super::utils::{self, RefundsRequestData};
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::settings,
|
configs::settings,
|
||||||
core::errors::{self, CustomResult},
|
core::errors::{self, CustomResult},
|
||||||
|
db::StorageInterface,
|
||||||
headers,
|
headers,
|
||||||
services::{self, ConnectorIntegration},
|
services::{self, ConnectorIntegration},
|
||||||
types::{
|
types::{
|
||||||
@ -20,7 +22,7 @@ use crate::{
|
|||||||
api::{self, ConnectorCommon, ConnectorCommonExt},
|
api::{self, ConnectorCommon, ConnectorCommonExt},
|
||||||
ErrorResponse, Response,
|
ErrorResponse, Response,
|
||||||
},
|
},
|
||||||
utils::{self, BytesExt},
|
utils::{self as ext_traits, BytesExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -384,7 +386,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
|||||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
let connector_req = WorldpayPaymentsRequest::try_from(req)?;
|
let connector_req = WorldpayPaymentsRequest::try_from(req)?;
|
||||||
let worldpay_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)?;
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
Ok(Some(worldpay_req))
|
Ok(Some(worldpay_req))
|
||||||
}
|
}
|
||||||
@ -458,8 +460,9 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
|
|||||||
req: &types::RefundExecuteRouterData,
|
req: &types::RefundExecuteRouterData,
|
||||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
let connector_req = WorldpayRefundRequest::try_from(req)?;
|
let connector_req = WorldpayRefundRequest::try_from(req)?;
|
||||||
let req = utils::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req)
|
let req =
|
||||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
ext_traits::Encode::<WorldpayRefundRequest>::encode_to_string_of_json(&connector_req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
Ok(Some(req))
|
Ok(Some(req))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,24 +596,109 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
|
|||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl api::IncomingWebhook for Worldpay {
|
impl api::IncomingWebhook for Worldpay {
|
||||||
fn get_webhook_object_reference_id(
|
fn get_webhook_source_verification_algorithm(
|
||||||
&self,
|
&self,
|
||||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
_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> {
|
) -> 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(
|
fn get_webhook_event_type(
|
||||||
&self,
|
&self,
|
||||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
) -> 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(
|
fn get_webhook_resource_object(
|
||||||
&self,
|
&self,
|
||||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
) -> 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,8 @@ pub enum EventType {
|
|||||||
Refused,
|
Refused,
|
||||||
Refunded,
|
Refunded,
|
||||||
Error,
|
Error,
|
||||||
|
SentForSettlement,
|
||||||
|
Expired,
|
||||||
CaptureFailed,
|
CaptureFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,3 +307,39 @@ pub struct WorldpayErrorResponse {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
pub validation_errors: Option<serde_json::Value>,
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@ -122,7 +122,7 @@ impl From<EventType> for enums::AttemptStatus {
|
|||||||
EventType::Authorized => Self::Authorized,
|
EventType::Authorized => Self::Authorized,
|
||||||
EventType::CaptureFailed => Self::CaptureFailed,
|
EventType::CaptureFailed => Self::CaptureFailed,
|
||||||
EventType::Refused => Self::Failure,
|
EventType::Refused => Self::Failure,
|
||||||
EventType::Charged => Self::Charged,
|
EventType::Charged | EventType::SentForSettlement => Self::Charged,
|
||||||
_ => Self::Pending,
|
_ => 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
pub mod transformers;
|
pub mod transformers;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use error_stack::{IntoReport, ResultExt};
|
use error_stack::{IntoReport, ResultExt};
|
||||||
use masking::ExposeInterface;
|
use masking::ExposeInterface;
|
||||||
use router_env::{instrument, tracing};
|
use router_env::{instrument, tracing};
|
||||||
@ -488,20 +486,10 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
|||||||
.attach_printable("Failed construction of ConnectorData")?;
|
.attach_printable("Failed construction of ConnectorData")?;
|
||||||
|
|
||||||
let connector = connector.connector;
|
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 {
|
let mut request_details = api::IncomingWebhookRequestDetails {
|
||||||
method: req.method().clone(),
|
method: req.method().clone(),
|
||||||
headers: req.headers(),
|
headers: req.headers(),
|
||||||
query_params: req.query_string().to_string(),
|
query_params: req.query_string().to_string(),
|
||||||
query_params_json: json.as_bytes(),
|
|
||||||
body: &body,
|
body: &body,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,6 @@ pub struct IncomingWebhookRequestDetails<'a> {
|
|||||||
pub headers: &'a actix_web::http::header::HeaderMap,
|
pub headers: &'a actix_web::http::header::HeaderMap,
|
||||||
pub body: &'a [u8],
|
pub body: &'a [u8],
|
||||||
pub query_params: String,
|
pub query_params: String,
|
||||||
pub query_params_json: &'a [u8],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|||||||
Reference in New Issue
Block a user