mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +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,
|
||||
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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
Reference in New Issue
Block a user