feat(connector): add webhook support for worldline connector (#721)

This commit is contained in:
Arjun Karthik
2023-03-14 14:36:27 +05:30
committed by GitHub
parent 585618e51d
commit 13a8ce8ebc
5 changed files with 207 additions and 73 deletions

View File

@ -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>,
{ {

View File

@ -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)
} }
} }

View File

@ -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,
}

View File

@ -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")]

View File

@ -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)