Skip to content

Commit

Permalink
feat(core): add support for webhook additional source verification ca…
Browse files Browse the repository at this point in the history
…ll for paypal (#2058)

Signed-off-by: chikke srujan <[email protected]>
  • Loading branch information
srujanchikke authored Sep 20, 2023
1 parent 8ee2ce1 commit 2a9e09d
Show file tree
Hide file tree
Showing 13 changed files with 719 additions and 77 deletions.
3 changes: 3 additions & 0 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ locker_signing_key_id = "1" # Key_id to sign basilisk hs locker
[delayed_session_response]
connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which has delayed session response

[webhook_source_verification_call]
connectors_with_webhook_source_verification_call = "paypal" # List of connectors which has additional source verification api-call

[jwekey] # 4 priv/pub key pair
locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk
locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk
Expand Down
3 changes: 3 additions & 0 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ discord_invite_url = "https://discord.gg/wJZ7DVW8mm"
[delayed_session_response]
connectors_with_delayed_session_response = "trustpay,payme"

[webhook_source_verification_call]
connectors_with_webhook_source_verification_call = "paypal"

[mandates.supported_payment_methods]
pay_later.klarna = { connector_list = "adyen" }
wallet.google_pay = { connector_list = "stripe,adyen" }
Expand Down
2 changes: 2 additions & 0 deletions config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ cards = [
[delayed_session_response]
connectors_with_delayed_session_response = "trustpay,payme"

[webhook_source_verification_call]
connectors_with_webhook_source_verification_call = "paypal"

[scheduler]
stream = "SCHEDULER_STREAM"
Expand Down
1 change: 1 addition & 0 deletions crates/api_models/src/webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub enum OutgoingWebhookContent {
DisputeDetails(Box<disputes::DisputeResponse>),
}

#[derive(Debug, Clone, Serialize)]
pub struct ConnectorWebhookSecrets {
pub secret: Vec<u8>,
pub additional_secret: Option<masking::Secret<String>>,
Expand Down
7 changes: 7 additions & 0 deletions crates/router/src/configs/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pub struct Settings {
pub mandates: Mandates,
pub required_fields: RequiredFields,
pub delayed_session_response: DelayedSessionConfig,
pub webhook_source_verification_call: WebhookSourceVerificationCall,
pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig,
#[cfg(feature = "payouts")]
pub payouts: Payouts,
Expand Down Expand Up @@ -630,6 +631,12 @@ pub struct DelayedSessionConfig {
pub connectors_with_delayed_session_response: HashSet<api_models::enums::Connector>,
}

#[derive(Debug, Deserialize, Clone, Default)]
pub struct WebhookSourceVerificationCall {
#[serde(deserialize_with = "connector_deser")]
pub connectors_with_webhook_source_verification_call: HashSet<api_models::enums::Connector>,
}

#[derive(Debug, Deserialize, Clone, Default)]
pub struct ApplePayDecryptConifg {
pub apple_pay_ppc: String,
Expand Down
194 changes: 150 additions & 44 deletions crates/router/src/connector/paypal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fmt::Debug;
use base64::Engine;
use common_utils::ext_traits::ByteSliceExt;
use diesel_models::enums;
use error_stack::ResultExt;
use error_stack::{IntoReport, ResultExt};
use masking::PeekInterface;
use transformers as paypal;

Expand All @@ -28,8 +28,8 @@ use crate::{
},
types::{
self,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt},
domain, ErrorResponse, Response,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource},
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
Expand All @@ -50,6 +50,7 @@ impl api::PaymentVoid for Paypal {}
impl api::Refund for Paypal {}
impl api::RefundExecute for Paypal {}
impl api::RefundSync for Paypal {}
impl api::ConnectorVerifyWebhookSource for Paypal {}

impl Paypal {
pub fn get_order_error_response(
Expand Down Expand Up @@ -569,31 +570,15 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
match data.payment_method {
diesel_models::enums::PaymentMethod::Wallet
| diesel_models::enums::PaymentMethod::BankRedirect => {
let response: paypal::PaypalSyncResponse = res
.response
.parse_struct("paypal SyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
_ => {
let response: paypal::PaypalPaymentsSyncResponse = res
.response
.parse_struct("paypal PaymentsSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
}
let response: paypal::PaypalSyncResponse = res
.response
.parse_struct("paypal SyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}

fn get_error_response(
Expand Down Expand Up @@ -909,18 +894,123 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
}
}

#[async_trait::async_trait]
impl api::IncomingWebhook for Paypal {
async fn verify_webhook_source(
impl
ConnectorIntegration<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
> for Paypal
{
fn get_headers(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_account: &domain::MerchantAccount,
_merchant_connector_account: domain::MerchantConnectorAccount,
_connector_label: &str,
) -> CustomResult<bool, errors::ConnectorError> {
Ok(false) // Verify webhook source is not implemented for Paypal it requires additional apicall this function needs to be modified once we have a way to verify webhook source
req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth: paypal::PaypalAuthType = (&req.connector_auth_type)
.try_into()
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;

let auth_id = auth
.key1
.zip(auth.api_key)
.map(|(key1, api_key)| format!("{}:{}", key1, api_key));
let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek()));

Ok(vec![
(
headers::CONTENT_TYPE.to_string(),
types::VerifyWebhookSourceType::get_content_type(self)
.to_string()
.into(),
),
(headers::AUTHORIZATION.to_string(), auth_val.into_masked()),
])
}

fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}

fn get_url(
&self,
_req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}v1/notifications/verify-webhook-signature",
self.base_url(connectors)
))
}

fn build_request(
&self,
req: &types::VerifyWebhookSourceRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::VerifyWebhookSourceType::get_url(
self, req, connectors,
)?)
.headers(types::VerifyWebhookSourceType::get_headers(
self, req, connectors,
)?)
.body(types::VerifyWebhookSourceType::get_request_body(self, req)?)
.build();

Ok(Some(request))
}

fn get_request_body(
&self,
req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?;
let paypal_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<paypal::PaypalSourceVerificationRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(paypal_req))
}

fn handle_response(
&self,
data: &types::VerifyWebhookSourceRouterData,
res: Response,
) -> CustomResult<types::VerifyWebhookSourceRouterData, errors::ConnectorError> {
let response: paypal::PaypalSourceVerificationResponse = res
.response
.parse_struct("paypal PaypalSourceVerificationResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}

#[async_trait::async_trait]
impl api::IncomingWebhook for Paypal {
fn get_webhook_object_reference_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
Expand Down Expand Up @@ -972,13 +1062,29 @@ impl api::IncomingWebhook for Paypal {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
let details: paypal::PaypalWebhooksBody = request
.body
.parse_struct("PaypalWebooksEventType")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
let res_json = utils::Encode::<transformers::PaypalWebhooksBody>::encode_to_value(&details)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(res_json)
let details: paypal::PaypalWebhooksBody =
request
.body
.parse_struct("PaypalWebhooksBody")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
let sync_payload = match details.resource {
paypal::PaypalResource::PaypalCardWebhooks(resource) => serde_json::to_value(
paypal::PaypalPaymentsSyncResponse::try_from((*resource, details.event_type))?,
)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?,
paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => serde_json::to_value(
paypal::PaypalOrdersResponse::try_from((*resource, details.event_type))?,
)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?,
paypal::PaypalResource::PaypalRefundWebhooks(resource) => serde_json::to_value(
paypal::RefundSyncResponse::try_from((*resource, details.event_type))?,
)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?,
};
Ok(sync_payload)
}
}

Expand Down
Loading

0 comments on commit 2a9e09d

Please sign in to comment.