mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 17:47:54 +08:00
feat(connector): add webhook support for worldline connector (#721)
This commit is contained in:
@ -176,17 +176,17 @@ impl<T> BytesExt<T> for bytes::Bytes {
|
|||||||
///
|
///
|
||||||
/// Extending functionalities of `[u8]` for performing parsing
|
/// Extending functionalities of `[u8]` for performing parsing
|
||||||
///
|
///
|
||||||
pub trait ByteSliceExt<T> {
|
pub trait ByteSliceExt {
|
||||||
///
|
///
|
||||||
/// Convert `[u8]` into type `<T>` by using `serde::Deserialize`
|
/// Convert `[u8]` into type `<T>` by using `serde::Deserialize`
|
||||||
///
|
///
|
||||||
fn parse_struct<'de>(&'de self, type_name: &str) -> CustomResult<T, errors::ParsingError>
|
fn parse_struct<'de, T>(&'de self, type_name: &str) -> CustomResult<T, errors::ParsingError>
|
||||||
where
|
where
|
||||||
T: Deserialize<'de>;
|
T: Deserialize<'de>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ByteSliceExt<T> for [u8] {
|
impl ByteSliceExt for [u8] {
|
||||||
fn parse_struct<'de>(&'de self, type_name: &str) -> CustomResult<T, errors::ParsingError>
|
fn parse_struct<'de, T>(&'de self, type_name: &str) -> CustomResult<T, errors::ParsingError>
|
||||||
where
|
where
|
||||||
T: Deserialize<'de>,
|
T: Deserialize<'de>,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,6 +3,7 @@ mod transformers;
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use common_utils::ext_traits::ByteSliceExt;
|
||||||
use error_stack::{IntoReport, ResultExt};
|
use error_stack::{IntoReport, ResultExt};
|
||||||
use ring::hmac;
|
use ring::hmac;
|
||||||
use storage_models::enums;
|
use storage_models::enums;
|
||||||
@ -12,8 +13,10 @@ use transformers as worldline;
|
|||||||
use super::utils::RefundsRequestData;
|
use super::utils::RefundsRequestData;
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::settings::Connectors,
|
configs::settings::Connectors,
|
||||||
|
connector::utils as conn_utils,
|
||||||
consts,
|
consts,
|
||||||
core::errors::{self, CustomResult},
|
core::errors::{self, CustomResult},
|
||||||
|
db::StorageInterface,
|
||||||
headers, logger,
|
headers, logger,
|
||||||
services::{self, ConnectorIntegration},
|
services::{self, ConnectorIntegration},
|
||||||
types::{
|
types::{
|
||||||
@ -21,7 +24,7 @@ use crate::{
|
|||||||
api::{self, ConnectorCommon, ConnectorCommonExt},
|
api::{self, ConnectorCommon, ConnectorCommonExt},
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
},
|
},
|
||||||
utils::{self, BytesExt},
|
utils::{self, crypto, BytesExt, OptionExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -651,27 +654,123 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_endpoint_verification(headers: &actix_web::http::header::HeaderMap) -> bool {
|
||||||
|
headers
|
||||||
|
.get("x-gcs-webhooks-endpoint-verification")
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl api::IncomingWebhook for Worldline {
|
impl api::IncomingWebhook for Worldline {
|
||||||
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::HmacSha256))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_source_verification_signature(
|
||||||
|
&self,
|
||||||
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
|
let header_value = conn_utils::get_header_key_value("X-GCS-Signature", request.headers)?;
|
||||||
|
let signature = consts::BASE64_ENGINE
|
||||||
|
.decode(header_value.as_bytes())
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_source_verification_message(
|
||||||
|
&self,
|
||||||
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
|
_merchant_id: &str,
|
||||||
|
_secret: &[u8],
|
||||||
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
|
Ok(request.body.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_webhook_source_verification_merchant_secret(
|
||||||
|
&self,
|
||||||
|
db: &dyn StorageInterface,
|
||||||
|
merchant_id: &str,
|
||||||
|
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
|
||||||
|
let key = format!("whsec_verification_{}_{}", self.id(), merchant_id);
|
||||||
|
let secret = db
|
||||||
|
.get_key(&key)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
|
||||||
|
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_object_reference_id(
|
||||||
|
&self,
|
||||||
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
) -> CustomResult<String, errors::ConnectorError> {
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
|| -> _ {
|
||||||
|
Ok(request
|
||||||
|
.body
|
||||||
|
.parse_struct::<worldline::WebhookBody>("WorldlineWebhookEvent")?
|
||||||
|
.payment
|
||||||
|
.parse_value::<worldline::Payment>("WorldlineWebhookObjectId")?
|
||||||
|
.id)
|
||||||
|
}()
|
||||||
|
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
if is_endpoint_verification(request.headers) {
|
||||||
|
Ok(api::IncomingWebhookEvent::EndpointVerification)
|
||||||
|
} else {
|
||||||
|
let details: worldline::WebhookBody = request
|
||||||
|
.body
|
||||||
|
.parse_struct("WorldlineWebhookObjectId")
|
||||||
|
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||||
|
let event = match details.event_type {
|
||||||
|
worldline::WebhookEvent::Paid => api::IncomingWebhookEvent::PaymentIntentSuccess,
|
||||||
|
worldline::WebhookEvent::Rejected | worldline::WebhookEvent::RejectedCapture => {
|
||||||
|
api::IncomingWebhookEvent::PaymentIntentFailure
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 details = request
|
||||||
|
.body
|
||||||
|
.parse_struct::<worldline::WebhookBody>("WorldlineWebhookObjectId")
|
||||||
|
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?
|
||||||
|
.payment
|
||||||
|
.ok_or(errors::ConnectorError::WebhookResourceObjectNotFound)?;
|
||||||
|
Ok(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_api_response(
|
||||||
|
&self,
|
||||||
|
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||||
|
) -> CustomResult<services::api::ApplicationResponse<serde_json::Value>, errors::ConnectorError>
|
||||||
|
{
|
||||||
|
let verification_header = request.headers.get("x-gcs-webhooks-endpoint-verification");
|
||||||
|
let response = match verification_header {
|
||||||
|
None => services::api::ApplicationResponse::StatusOk,
|
||||||
|
Some(header_value) => {
|
||||||
|
let verification_signature_value = header_value
|
||||||
|
.to_str()
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::WebhookResponseEncodingFailed)?
|
||||||
|
.to_string();
|
||||||
|
services::api::ApplicationResponse::TextPlain(verification_signature_value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -297,6 +297,7 @@ pub enum PaymentStatus {
|
|||||||
CaptureRequested,
|
CaptureRequested,
|
||||||
#[default]
|
#[default]
|
||||||
Processing,
|
Processing,
|
||||||
|
Created,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForeignFrom<(PaymentStatus, enums::CaptureMethod)> for enums::AttemptStatus {
|
impl ForeignFrom<(PaymentStatus, enums::CaptureMethod)> for enums::AttemptStatus {
|
||||||
@ -307,7 +308,8 @@ impl ForeignFrom<(PaymentStatus, enums::CaptureMethod)> for enums::AttemptStatus
|
|||||||
| PaymentStatus::Paid
|
| PaymentStatus::Paid
|
||||||
| PaymentStatus::ChargebackNotification => Self::Charged,
|
| PaymentStatus::ChargebackNotification => Self::Charged,
|
||||||
PaymentStatus::Cancelled => Self::Voided,
|
PaymentStatus::Cancelled => Self::Voided,
|
||||||
PaymentStatus::Rejected | PaymentStatus::RejectedCapture => Self::Failure,
|
PaymentStatus::Rejected => Self::Failure,
|
||||||
|
PaymentStatus::RejectedCapture => Self::CaptureFailed,
|
||||||
PaymentStatus::CaptureRequested => {
|
PaymentStatus::CaptureRequested => {
|
||||||
if capture_method == enums::CaptureMethod::Automatic {
|
if capture_method == enums::CaptureMethod::Automatic {
|
||||||
Self::Pending
|
Self::Pending
|
||||||
@ -316,6 +318,7 @@ impl ForeignFrom<(PaymentStatus, enums::CaptureMethod)> for enums::AttemptStatus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
PaymentStatus::PendingApproval => Self::Authorized,
|
PaymentStatus::PendingApproval => Self::Authorized,
|
||||||
|
PaymentStatus::Created => Self::Started,
|
||||||
_ => Self::Pending,
|
_ => Self::Pending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -326,8 +329,8 @@ impl ForeignFrom<(PaymentStatus, enums::CaptureMethod)> for enums::AttemptStatus
|
|||||||
/// To keep this try_from logic generic in case of AUTHORIZE, SYNC and CAPTURE flows capture_method will be set from RouterData request.
|
/// To keep this try_from logic generic in case of AUTHORIZE, SYNC and CAPTURE flows capture_method will be set from RouterData request.
|
||||||
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
|
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
|
||||||
pub struct Payment {
|
pub struct Payment {
|
||||||
id: String,
|
pub id: String,
|
||||||
status: PaymentStatus,
|
pub status: PaymentStatus,
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
pub capture_method: enums::CaptureMethod,
|
pub capture_method: enums::CaptureMethod,
|
||||||
}
|
}
|
||||||
@ -486,3 +489,28 @@ pub struct ErrorResponse {
|
|||||||
pub error_id: Option<String>,
|
pub error_id: Option<String>,
|
||||||
pub errors: Vec<Error>,
|
pub errors: Vec<Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WebhookBody {
|
||||||
|
pub api_version: Option<String>,
|
||||||
|
pub id: String,
|
||||||
|
pub created: String,
|
||||||
|
pub merchant_id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub event_type: WebhookEvent,
|
||||||
|
pub payment: Option<serde_json::Value>,
|
||||||
|
pub refund: Option<serde_json::Value>,
|
||||||
|
pub payout: Option<serde_json::Value>,
|
||||||
|
pub token: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub enum WebhookEvent {
|
||||||
|
#[serde(rename = "payment.rejected")]
|
||||||
|
Rejected,
|
||||||
|
#[serde(rename = "payment.rejected_capture")]
|
||||||
|
RejectedCapture,
|
||||||
|
#[serde(rename = "payment.paid")]
|
||||||
|
Paid,
|
||||||
|
}
|
||||||
|
|||||||
@ -281,6 +281,8 @@ pub enum ConnectorError {
|
|||||||
WebhookEventTypeNotFound,
|
WebhookEventTypeNotFound,
|
||||||
#[error("Incoming webhook event resource object not found")]
|
#[error("Incoming webhook event resource object not found")]
|
||||||
WebhookResourceObjectNotFound,
|
WebhookResourceObjectNotFound,
|
||||||
|
#[error("Could not respond to the incoming webhook event")]
|
||||||
|
WebhookResponseEncodingFailed,
|
||||||
#[error("Invalid Date/time format")]
|
#[error("Invalid Date/time format")]
|
||||||
InvalidDateFormat,
|
InvalidDateFormat,
|
||||||
#[error("Invalid Data format")]
|
#[error("Invalid Data format")]
|
||||||
|
|||||||
@ -298,16 +298,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
|||||||
body: &body,
|
body: &body,
|
||||||
};
|
};
|
||||||
|
|
||||||
let source_verified = connector
|
|
||||||
.verify_webhook_source(
|
|
||||||
&*state.store,
|
|
||||||
&request_details,
|
|
||||||
&merchant_account.merchant_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
||||||
.attach_printable("There was an issue in incoming webhook source verification")?;
|
|
||||||
|
|
||||||
let decoded_body = connector
|
let decoded_body = connector
|
||||||
.decode_webhook_body(
|
.decode_webhook_body(
|
||||||
&*state.store,
|
&*state.store,
|
||||||
@ -325,6 +315,20 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
|||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable("Could not find event type in incoming webhook body")?;
|
.attach_printable("Could not find event type in incoming webhook body")?;
|
||||||
|
|
||||||
|
if !matches!(
|
||||||
|
event_type,
|
||||||
|
api_models::webhooks::IncomingWebhookEvent::EndpointVerification
|
||||||
|
) {
|
||||||
|
let source_verified = connector
|
||||||
|
.verify_webhook_source(
|
||||||
|
&*state.store,
|
||||||
|
&request_details,
|
||||||
|
&merchant_account.merchant_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("There was an issue in incoming webhook source verification")?;
|
||||||
|
|
||||||
let process_webhook_further = utils::lookup_webhook_event(
|
let process_webhook_further = utils::lookup_webhook_event(
|
||||||
&*state.store,
|
&*state.store,
|
||||||
connector_name,
|
connector_name,
|
||||||
@ -384,6 +388,7 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
|
|||||||
.attach_printable("Unsupported Flow Type received in incoming webhooks")?,
|
.attach_printable("Unsupported Flow Type received in incoming webhooks")?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let response = connector
|
let response = connector
|
||||||
.get_webhook_api_response(&request_details)
|
.get_webhook_api_response(&request_details)
|
||||||
|
|||||||
Reference in New Issue
Block a user