From c14b18336e52e6dd0db07b5328ca8960ef175b2a Mon Sep 17 00:00:00 2001 From: Sk Sakil Mostak Date: Fri, 30 Aug 2024 13:23:16 +0530 Subject: [PATCH 1/3] feat: add webhooks for payout --- crates/router/src/connector/adyenplatform.rs | 111 ++++++++++++++++-- .../adyenplatform/transformers/payouts.rs | 98 +++++++++++++++- 2 files changed, 197 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/adyenplatform.rs b/crates/router/src/connector/adyenplatform.rs index 2f8cde282da6..6e392666078f 100644 --- a/crates/router/src/connector/adyenplatform.rs +++ b/crates/router/src/connector/adyenplatform.rs @@ -1,12 +1,17 @@ pub mod transformers; +use api_models::{self, webhooks::IncomingWebhookEvent}; +use base64::Engine; #[cfg(feature = "payouts")] use common_utils::request::RequestContent; #[cfg(feature = "payouts")] use common_utils::types::MinorUnitForConnector; #[cfg(feature = "payouts")] use common_utils::types::{AmountConvertor, MinorUnit}; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; +use http::HeaderName; +use masking::Secret; +use ring::hmac; #[cfg(feature = "payouts")] use router_env::{instrument, tracing}; @@ -25,10 +30,12 @@ use crate::{ types::{ self, api::{self, ConnectorCommon}, + transformers::ForeignFrom, }, + utils::{crypto, ByteSliceExt, BytesExt}, }; #[cfg(feature = "payouts")] -use crate::{events::connector_api_logs::ConnectorEvent, utils::BytesExt}; +use crate::{consts, events::connector_api_logs::ConnectorEvent}; #[derive(Clone)] pub struct Adyenplatform { @@ -294,24 +301,110 @@ impl services::ConnectorIntegration, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let base64_signature = request + .headers + .get(HeaderName::from_static("hmacsignature")) + .ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)?; + Ok(base64_signature.as_bytes().to_vec()) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &common_utils::id_type::MerchantId, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + Ok(request.body.to_vec()) + } + + async fn verify_webhook_source( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + merchant_id: &common_utils::id_type::MerchantId, + connector_webhook_details: Option, + _connector_account_details: crypto::Encryptable>, + connector_label: &str, + ) -> CustomResult { + let connector_webhook_secrets = self + .get_webhook_source_verification_merchant_secret( + merchant_id, + connector_label, + connector_webhook_details, + ) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let message = self + .get_webhook_source_verification_message( + request, + merchant_id, + &connector_webhook_secrets, + ) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let raw_key = hex::decode(connector_webhook_secrets.secret) + .change_context(errors::ConnectorError::WebhookVerificationSecretInvalid)?; + + let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &raw_key); + let signed_messaged = hmac::sign(&signing_key, &message); + let payload_sign = consts::BASE64_ENGINE.encode(signed_messaged.as_ref()); + Ok(payload_sign.as_bytes().eq(&signature)) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request + .body + .parse_struct("AdyenplatformIncomingWebhook") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + Ok(api_models::webhooks::ObjectReferenceId::PayoutId( + api_models::webhooks::PayoutIdType::PayoutAttemptId(webhook_body.data.reference), + )) } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request + .body + .parse_struct("AdyenplatformIncomingWebhook") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + Ok(IncomingWebhookEvent::foreign_from(( + webhook_body.webhook_type, + webhook_body.data.status, + webhook_body.data.tracking, + ))) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request + .body + .parse_struct("AdyenplatformIncomingWebhook") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + Ok(Box::new(webhook_body)) } } diff --git a/crates/router/src/connector/adyenplatform/transformers/payouts.rs b/crates/router/src/connector/adyenplatform/transformers/payouts.rs index aac51e5e8211..882f20050631 100644 --- a/crates/router/src/connector/adyenplatform/transformers/payouts.rs +++ b/crates/router/src/connector/adyenplatform/transformers/payouts.rs @@ -1,3 +1,4 @@ +use api_models::webhooks; use common_utils::pii; use error_stack::{report, ResultExt}; use masking::Secret; @@ -10,7 +11,7 @@ use crate::{ utils::{self, PayoutsData, RouterData}, }, core::errors, - types::{self, api::payouts, storage::enums}, + types::{self, api::payouts, storage::enums, transformers::ForeignFrom}, }; #[derive(Debug, Default, Serialize, Deserialize)] @@ -251,7 +252,7 @@ impl TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData>> for Adye category: AdyenPayoutMethod::try_from(payout_type)?, counterparty, priority: AdyenPayoutPriority::from(priority), - reference: request.payout_id.clone(), + reference: item.router_data.connector_request_reference_id.clone(), reference_for_beneficiary: request.payout_id, description: item.router_data.description.clone(), }) @@ -286,7 +287,7 @@ impl TryFrom> impl From for enums::PayoutStatus { fn from(adyen_status: AdyenTransferStatus) -> Self { match adyen_status { - AdyenTransferStatus::Authorised => Self::Success, + AdyenTransferStatus::Authorised => Self::Initiated, AdyenTransferStatus::Refused => Self::Ineligible, AdyenTransferStatus::Error => Self::Failed, } @@ -336,6 +337,97 @@ impl TryFrom for AdyenPayoutMethod { } } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenplatformIncomingWebhook { + pub data: AdyenplatformIncomingWebhookData, + #[serde(rename = "type")] + pub webhook_type: AdyenplatformWebhookEventType, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenplatformIncomingWebhookData { + pub status: AdyenplatformWebhookStatus, + pub reference: String, + pub priority: AdyenPayoutPriority, + pub tracking: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenplatformInstantStatus { + status: InstantPriorityStatus, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum InstantPriorityStatus { + Pending, + Credited, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum AdyenplatformWebhookEventType { + #[serde(rename = "balancePlatform.transfer.created")] + PayoutCreated, + #[serde(rename = "balancePlatform.transfer.updated")] + PayoutUpdated, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AdyenplatformWebhookStatus { + Authorised, + Booked, + Pending, + Failed, + Returned, + Received, +} + +impl + ForeignFrom<( + AdyenplatformWebhookEventType, + AdyenplatformWebhookStatus, + Option, + )> for webhooks::IncomingWebhookEvent +{ + fn foreign_from( + (event_type, status, instant_status): ( + AdyenplatformWebhookEventType, + AdyenplatformWebhookStatus, + Option, + ), + ) -> Self { + match (event_type, status, instant_status) { + (AdyenplatformWebhookEventType::PayoutCreated, _, _) => Self::PayoutCreated, + ( + AdyenplatformWebhookEventType::PayoutUpdated, + _, + Some(AdyenplatformInstantStatus { + status: InstantPriorityStatus::Credited, + }), + ) => Self::PayoutSuccess, + ( + AdyenplatformWebhookEventType::PayoutUpdated, + _, + Some(AdyenplatformInstantStatus { + status: InstantPriorityStatus::Pending, + }), + ) => Self::PayoutProcessing, + (AdyenplatformWebhookEventType::PayoutUpdated, status, _) => match status { + AdyenplatformWebhookStatus::Authorised + | AdyenplatformWebhookStatus::Booked + | AdyenplatformWebhookStatus::Received => Self::PayoutCreated, + AdyenplatformWebhookStatus::Pending => Self::PayoutProcessing, + AdyenplatformWebhookStatus::Failed => Self::PayoutFailure, + AdyenplatformWebhookStatus::Returned => Self::PayoutReversed, + }, + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenTransferErrorResponse { From c917b95003b31f645dc3385f627fb62b77ed3dbf Mon Sep 17 00:00:00 2001 From: Sk Sakil Mostak Date: Fri, 30 Aug 2024 16:11:47 +0530 Subject: [PATCH 2/3] refactor: resolve ci checks --- crates/router/src/connector/adyenplatform.rs | 96 +++++++++++++------ .../adyenplatform/transformers/payouts.rs | 8 ++ 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/crates/router/src/connector/adyenplatform.rs b/crates/router/src/connector/adyenplatform.rs index 6e392666078f..f594bdc09516 100644 --- a/crates/router/src/connector/adyenplatform.rs +++ b/crates/router/src/connector/adyenplatform.rs @@ -1,6 +1,6 @@ pub mod transformers; - use api_models::{self, webhooks::IncomingWebhookEvent}; +#[cfg(feature = "payouts")] use base64::Engine; #[cfg(feature = "payouts")] use common_utils::request::RequestContent; @@ -8,9 +8,14 @@ use common_utils::request::RequestContent; use common_utils::types::MinorUnitForConnector; #[cfg(feature = "payouts")] use common_utils::types::{AmountConvertor, MinorUnit}; +#[cfg(not(feature = "payouts"))] +use error_stack::report; use error_stack::ResultExt; +#[cfg(feature = "payouts")] use http::HeaderName; +#[cfg(feature = "payouts")] use masking::Secret; +#[cfg(feature = "payouts")] use ring::hmac; #[cfg(feature = "payouts")] use router_env::{instrument, tracing}; @@ -30,12 +35,15 @@ use crate::{ types::{ self, api::{self, ConnectorCommon}, - transformers::ForeignFrom, }, - utils::{crypto, ByteSliceExt, BytesExt}, }; #[cfg(feature = "payouts")] -use crate::{consts, events::connector_api_logs::ConnectorEvent}; +use crate::{ + consts, + events::connector_api_logs::ConnectorEvent, + types::transformers::ForeignFrom, + utils::{crypto, ByteSliceExt, BytesExt}, +}; #[derive(Clone)] pub struct Adyenplatform { @@ -301,6 +309,7 @@ impl services::ConnectorIntegration, @@ -308,6 +317,7 @@ impl api::IncomingWebhook for Adyenplatform { Ok(Box::new(crypto::HmacSha256)) } + #[cfg(feature = "payouts")] fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, @@ -320,6 +330,7 @@ impl api::IncomingWebhook for Adyenplatform { Ok(base64_signature.as_bytes().to_vec()) } + #[cfg(feature = "payouts")] fn get_webhook_source_verification_message( &self, request: &api::IncomingWebhookRequestDetails<'_>, @@ -329,6 +340,7 @@ impl api::IncomingWebhook for Adyenplatform { Ok(request.body.to_vec()) } + #[cfg(feature = "payouts")] async fn verify_webhook_source( &self, request: &api::IncomingWebhookRequestDetails<'_>, @@ -369,42 +381,66 @@ impl api::IncomingWebhook for Adyenplatform { fn get_webhook_object_reference_id( &self, - request: &api::IncomingWebhookRequestDetails<'_>, + #[cfg(feature = "payouts")] request: &api::IncomingWebhookRequestDetails<'_>, + #[cfg(not(feature = "payouts"))] _request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request - .body - .parse_struct("AdyenplatformIncomingWebhook") - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - - Ok(api_models::webhooks::ObjectReferenceId::PayoutId( - api_models::webhooks::PayoutIdType::PayoutAttemptId(webhook_body.data.reference), - )) + #[cfg(feature = "payouts")] + { + let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request + .body + .parse_struct("AdyenplatformIncomingWebhook") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + return Ok(api_models::webhooks::ObjectReferenceId::PayoutId( + api_models::webhooks::PayoutIdType::PayoutAttemptId(webhook_body.data.reference), + )); + } + #[cfg(not(feature = "payouts"))] + { + return Err(report!(errors::ConnectorError::WebhooksNotImplemented)); + } } fn get_webhook_event_type( &self, - request: &api::IncomingWebhookRequestDetails<'_>, + #[cfg(feature = "payouts")] request: &api::IncomingWebhookRequestDetails<'_>, + #[cfg(not(feature = "payouts"))] _request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request - .body - .parse_struct("AdyenplatformIncomingWebhook") - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - - Ok(IncomingWebhookEvent::foreign_from(( - webhook_body.webhook_type, - webhook_body.data.status, - webhook_body.data.tracking, - ))) + #[cfg(feature = "payouts")] + { + let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request + .body + .parse_struct("AdyenplatformIncomingWebhook") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + return Ok(IncomingWebhookEvent::foreign_from(( + webhook_body.webhook_type, + webhook_body.data.status, + webhook_body.data.tracking, + ))); + } + #[cfg(not(feature = "payouts"))] + { + return Err(report!(errors::ConnectorError::WebhooksNotImplemented)); + } } fn get_webhook_resource_object( &self, - request: &api::IncomingWebhookRequestDetails<'_>, + #[cfg(feature = "payouts")] request: &api::IncomingWebhookRequestDetails<'_>, + #[cfg(not(feature = "payouts"))] _request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request - .body - .parse_struct("AdyenplatformIncomingWebhook") - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - Ok(Box::new(webhook_body)) + #[cfg(feature = "payouts")] + { + let webhook_body: adyenplatform::AdyenplatformIncomingWebhook = request + .body + .parse_struct("AdyenplatformIncomingWebhook") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + return Ok(Box::new(webhook_body)); + } + #[cfg(not(feature = "payouts"))] + { + return Err(report!(errors::ConnectorError::WebhooksNotImplemented)); + } } } diff --git a/crates/router/src/connector/adyenplatform/transformers/payouts.rs b/crates/router/src/connector/adyenplatform/transformers/payouts.rs index 882f20050631..e0ec8f9e18b7 100644 --- a/crates/router/src/connector/adyenplatform/transformers/payouts.rs +++ b/crates/router/src/connector/adyenplatform/transformers/payouts.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "payouts")] use api_models::webhooks; use common_utils::pii; use error_stack::{report, ResultExt}; @@ -337,6 +338,7 @@ impl TryFrom for AdyenPayoutMethod { } } +#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenplatformIncomingWebhook { @@ -345,6 +347,7 @@ pub struct AdyenplatformIncomingWebhook { pub webhook_type: AdyenplatformWebhookEventType, } +#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenplatformIncomingWebhookData { @@ -354,12 +357,14 @@ pub struct AdyenplatformIncomingWebhookData { pub tracking: Option, } +#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenplatformInstantStatus { status: InstantPriorityStatus, } +#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum InstantPriorityStatus { @@ -367,6 +372,7 @@ pub enum InstantPriorityStatus { Credited, } +#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] pub enum AdyenplatformWebhookEventType { #[serde(rename = "balancePlatform.transfer.created")] @@ -375,6 +381,7 @@ pub enum AdyenplatformWebhookEventType { PayoutUpdated, } +#[cfg(feature = "payouts")] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum AdyenplatformWebhookStatus { @@ -386,6 +393,7 @@ pub enum AdyenplatformWebhookStatus { Received, } +#[cfg(feature = "payouts")] impl ForeignFrom<( AdyenplatformWebhookEventType, From 27cd0e8698b8046725dbf82b4c52a252b06da3b5 Mon Sep 17 00:00:00 2001 From: Sk Sakil Mostak Date: Mon, 2 Sep 2024 15:32:27 +0530 Subject: [PATCH 3/3] refactor: ci checks --- crates/router/src/connector/adyenplatform.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/adyenplatform.rs b/crates/router/src/connector/adyenplatform.rs index f594bdc09516..3da1a2d33be2 100644 --- a/crates/router/src/connector/adyenplatform.rs +++ b/crates/router/src/connector/adyenplatform.rs @@ -391,13 +391,13 @@ impl api::IncomingWebhook for Adyenplatform { .parse_struct("AdyenplatformIncomingWebhook") .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - return Ok(api_models::webhooks::ObjectReferenceId::PayoutId( + Ok(api_models::webhooks::ObjectReferenceId::PayoutId( api_models::webhooks::PayoutIdType::PayoutAttemptId(webhook_body.data.reference), - )); + )) } #[cfg(not(feature = "payouts"))] { - return Err(report!(errors::ConnectorError::WebhooksNotImplemented)); + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } } @@ -413,15 +413,15 @@ impl api::IncomingWebhook for Adyenplatform { .parse_struct("AdyenplatformIncomingWebhook") .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - return Ok(IncomingWebhookEvent::foreign_from(( + Ok(IncomingWebhookEvent::foreign_from(( webhook_body.webhook_type, webhook_body.data.status, webhook_body.data.tracking, - ))); + ))) } #[cfg(not(feature = "payouts"))] { - return Err(report!(errors::ConnectorError::WebhooksNotImplemented)); + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } } @@ -436,11 +436,11 @@ impl api::IncomingWebhook for Adyenplatform { .body .parse_struct("AdyenplatformIncomingWebhook") .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - return Ok(Box::new(webhook_body)); + Ok(Box::new(webhook_body)) } #[cfg(not(feature = "payouts"))] { - return Err(report!(errors::ConnectorError::WebhooksNotImplemented)); + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } } }