mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(core): add support for webhook additional source verification call for paypal (#2058)
Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
This commit is contained in:
@ -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
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>>,
|
||||
|
||||
@ -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,
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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},
|
||||
};
|
||||
@ -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(
|
||||
@ -569,9 +570,6 @@ 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")
|
||||
@ -582,19 +580,6 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
@ -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<'_>,
|
||||
@ -972,13 +1062,29 @@ impl api::IncomingWebhook for Paypal {
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
let details: paypal::PaypalWebhooksBody = request
|
||||
let details: paypal::PaypalWebhooksBody =
|
||||
request
|
||||
.body
|
||||
.parse_struct("PaypalWebooksEventType")
|
||||
.parse_struct("PaypalWebhooksBody")
|
||||
.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 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use api_models::payments::BankRedirectData;
|
||||
use common_utils::errors::CustomResult;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
@ -11,9 +12,20 @@ use crate::{
|
||||
},
|
||||
core::errors,
|
||||
services,
|
||||
types::{self, api, storage::enums as storage_enums, transformers::ForeignFrom},
|
||||
types::{
|
||||
self, api, storage::enums as storage_enums, transformers::ForeignFrom,
|
||||
VerifyWebhookSourceResponseData,
|
||||
},
|
||||
};
|
||||
|
||||
mod webhook_headers {
|
||||
pub const PAYPAL_TRANSMISSION_ID: &str = "paypal-transmission-id";
|
||||
pub const PAYPAL_TRANSMISSION_TIME: &str = "paypal-transmission-time";
|
||||
pub const PAYPAL_TRANSMISSION_SIG: &str = "paypal-transmission-sig";
|
||||
pub const PAYPAL_CERT_URL: &str = "paypal-cert-url";
|
||||
pub const PAYPAL_AUTH_ALGO: &str = "paypal-auth-algo";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum PaypalPaymentIntent {
|
||||
@ -590,8 +602,8 @@ pub struct PaymentsCollection {
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PurchaseUnitItem {
|
||||
reference_id: String,
|
||||
payments: PaymentsCollection,
|
||||
pub reference_id: String,
|
||||
pub payments: PaymentsCollection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@ -621,6 +633,7 @@ pub struct PaypalRedirectResponse {
|
||||
pub enum PaypalSyncResponse {
|
||||
PaypalOrdersSyncResponse(PaypalOrdersResponse),
|
||||
PaypalRedirectSyncResponse(PaypalRedirectResponse),
|
||||
PaypalPaymentsSyncResponse(PaypalPaymentsSyncResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@ -771,6 +784,13 @@ impl<F, T> TryFrom<types::ResponseRouterData<F, PaypalSyncResponse, T, types::Pa
|
||||
http_code: item.http_code,
|
||||
})
|
||||
}
|
||||
PaypalSyncResponse::PaypalPaymentsSyncResponse(response) => {
|
||||
Self::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: item.data,
|
||||
http_code: item.http_code,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1050,7 +1070,7 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct RefundSyncResponse {
|
||||
id: String,
|
||||
status: RefundStatus,
|
||||
@ -1167,7 +1187,10 @@ pub struct PaypalCardWebhooks {
|
||||
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
pub struct PaypalRedirectsWebhooks {
|
||||
pub purchase_units: Vec<PaypalWebhooksPurchaseUnits>,
|
||||
pub purchase_units: Vec<PurchaseUnitItem>,
|
||||
pub links: Vec<PaypalLinks>,
|
||||
pub id: String,
|
||||
pub intent: PaypalPaymentIntent,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
@ -1196,13 +1219,222 @@ impl From<PaypalWebhookEventType> for api::IncomingWebhookEvent {
|
||||
PaypalWebhookEventType::PaymentCaptureCompleted
|
||||
| PaypalWebhookEventType::CheckoutOrderCompleted => Self::PaymentIntentSuccess,
|
||||
PaypalWebhookEventType::PaymentCapturePending
|
||||
| PaypalWebhookEventType::CheckoutOrderApproved
|
||||
| PaypalWebhookEventType::CheckoutOrderProcessed => Self::PaymentIntentProcessing,
|
||||
PaypalWebhookEventType::PaymentCaptureDeclined => Self::PaymentIntentFailure,
|
||||
PaypalWebhookEventType::PaymentCaptureRefunded => Self::RefundSuccess,
|
||||
PaypalWebhookEventType::Unknown
|
||||
| PaypalWebhookEventType::PaymentAuthorizationCreated
|
||||
| PaypalWebhookEventType::PaymentAuthorizationVoided => Self::EventNotSupported,
|
||||
PaypalWebhookEventType::PaymentAuthorizationCreated
|
||||
| PaypalWebhookEventType::PaymentAuthorizationVoided
|
||||
| PaypalWebhookEventType::CheckoutOrderApproved
|
||||
| PaypalWebhookEventType::Unknown => Self::EventNotSupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct PaypalSourceVerificationRequest {
|
||||
pub transmission_id: String,
|
||||
pub transmission_time: String,
|
||||
pub cert_url: String,
|
||||
pub transmission_sig: String,
|
||||
pub auth_algo: String,
|
||||
pub webhook_id: String,
|
||||
pub webhook_event: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
|
||||
pub struct PaypalSourceVerificationResponse {
|
||||
pub verification_status: PaypalSourceVerificationStatus,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaypalSourceVerificationStatus {
|
||||
Success,
|
||||
Failure,
|
||||
}
|
||||
|
||||
impl
|
||||
TryFrom<
|
||||
types::ResponseRouterData<
|
||||
api::VerifyWebhookSource,
|
||||
PaypalSourceVerificationResponse,
|
||||
types::VerifyWebhookSourceRequestData,
|
||||
VerifyWebhookSourceResponseData,
|
||||
>,
|
||||
> for types::VerifyWebhookSourceRouterData
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
api::VerifyWebhookSource,
|
||||
PaypalSourceVerificationResponse,
|
||||
types::VerifyWebhookSourceRequestData,
|
||||
VerifyWebhookSourceResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
response: Ok(VerifyWebhookSourceResponseData {
|
||||
verify_webhook_status: types::VerifyWebhookStatus::from(
|
||||
item.response.verification_status,
|
||||
),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PaypalSourceVerificationStatus> for types::VerifyWebhookStatus {
|
||||
fn from(item: PaypalSourceVerificationStatus) -> Self {
|
||||
match item {
|
||||
PaypalSourceVerificationStatus::Success => Self::SourceVerified,
|
||||
PaypalSourceVerificationStatus::Failure => Self::SourceNotVerified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(PaypalCardWebhooks, PaypalWebhookEventType)> for PaypalPaymentsSyncResponse {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
(webhook_body, webhook_event): (PaypalCardWebhooks, PaypalWebhookEventType),
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
id: webhook_body.supplementary_data.related_ids.order_id.clone(),
|
||||
status: PaypalPaymentStatus::try_from(webhook_event)?,
|
||||
amount: webhook_body.amount,
|
||||
supplementary_data: webhook_body.supplementary_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(PaypalRedirectsWebhooks, PaypalWebhookEventType)> for PaypalOrdersResponse {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
(webhook_body, webhook_event): (PaypalRedirectsWebhooks, PaypalWebhookEventType),
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
id: webhook_body.id,
|
||||
intent: webhook_body.intent,
|
||||
status: PaypalOrderStatus::try_from(webhook_event)?,
|
||||
purchase_units: webhook_body.purchase_units,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(PaypalRefundWebhooks, PaypalWebhookEventType)> for RefundSyncResponse {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
(webhook_body, webhook_event): (PaypalRefundWebhooks, PaypalWebhookEventType),
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
id: webhook_body.id,
|
||||
status: RefundStatus::try_from(webhook_event)
|
||||
.attach_printable("Could not find suitable webhook event")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PaypalWebhookEventType> for PaypalPaymentStatus {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(event: PaypalWebhookEventType) -> Result<Self, Self::Error> {
|
||||
match event {
|
||||
PaypalWebhookEventType::PaymentCaptureCompleted
|
||||
| PaypalWebhookEventType::CheckoutOrderCompleted => Ok(Self::Completed),
|
||||
PaypalWebhookEventType::PaymentAuthorizationVoided => Ok(Self::Voided),
|
||||
PaypalWebhookEventType::PaymentCaptureDeclined => Ok(Self::Declined),
|
||||
PaypalWebhookEventType::PaymentCapturePending
|
||||
| PaypalWebhookEventType::CheckoutOrderApproved
|
||||
| PaypalWebhookEventType::CheckoutOrderProcessed => Ok(Self::Pending),
|
||||
PaypalWebhookEventType::PaymentAuthorizationCreated => Ok(Self::Created),
|
||||
PaypalWebhookEventType::PaymentCaptureRefunded => Ok(Self::Refunded),
|
||||
PaypalWebhookEventType::Unknown => {
|
||||
Err(errors::ConnectorError::WebhookEventTypeNotFound.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PaypalWebhookEventType> for RefundStatus {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(event: PaypalWebhookEventType) -> Result<Self, Self::Error> {
|
||||
match event {
|
||||
PaypalWebhookEventType::PaymentCaptureRefunded => Ok(Self::Completed),
|
||||
PaypalWebhookEventType::PaymentAuthorizationCreated
|
||||
| PaypalWebhookEventType::PaymentAuthorizationVoided
|
||||
| PaypalWebhookEventType::PaymentCaptureDeclined
|
||||
| PaypalWebhookEventType::PaymentCaptureCompleted
|
||||
| PaypalWebhookEventType::PaymentCapturePending
|
||||
| PaypalWebhookEventType::CheckoutOrderApproved
|
||||
| PaypalWebhookEventType::CheckoutOrderCompleted
|
||||
| PaypalWebhookEventType::CheckoutOrderProcessed
|
||||
| PaypalWebhookEventType::Unknown => {
|
||||
Err(errors::ConnectorError::WebhookEventTypeNotFound.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PaypalWebhookEventType> for PaypalOrderStatus {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(event: PaypalWebhookEventType) -> Result<Self, Self::Error> {
|
||||
match event {
|
||||
PaypalWebhookEventType::PaymentCaptureCompleted
|
||||
| PaypalWebhookEventType::CheckoutOrderCompleted => Ok(Self::Completed),
|
||||
PaypalWebhookEventType::PaymentAuthorizationVoided => Ok(Self::Voided),
|
||||
PaypalWebhookEventType::PaymentCapturePending
|
||||
| PaypalWebhookEventType::CheckoutOrderProcessed => Ok(Self::Pending),
|
||||
PaypalWebhookEventType::PaymentAuthorizationCreated => Ok(Self::Created),
|
||||
PaypalWebhookEventType::CheckoutOrderApproved
|
||||
| PaypalWebhookEventType::PaymentCaptureDeclined
|
||||
| PaypalWebhookEventType::PaymentCaptureRefunded
|
||||
| PaypalWebhookEventType::Unknown => {
|
||||
Err(errors::ConnectorError::WebhookEventTypeNotFound.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&types::VerifyWebhookSourceRequestData> for PaypalSourceVerificationRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(req: &types::VerifyWebhookSourceRequestData) -> Result<Self, Self::Error> {
|
||||
let req_body = serde_json::from_slice(&req.webhook_body)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
Ok(Self {
|
||||
transmission_id: get_headers(
|
||||
&req.webhook_headers,
|
||||
webhook_headers::PAYPAL_TRANSMISSION_ID,
|
||||
)
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?,
|
||||
transmission_time: get_headers(
|
||||
&req.webhook_headers,
|
||||
webhook_headers::PAYPAL_TRANSMISSION_TIME,
|
||||
)?,
|
||||
cert_url: get_headers(&req.webhook_headers, webhook_headers::PAYPAL_CERT_URL)?,
|
||||
transmission_sig: get_headers(
|
||||
&req.webhook_headers,
|
||||
webhook_headers::PAYPAL_TRANSMISSION_SIG,
|
||||
)?,
|
||||
auth_algo: get_headers(&req.webhook_headers, webhook_headers::PAYPAL_AUTH_ALGO)?,
|
||||
webhook_id: String::from_utf8(req.merchant_secret.secret.to_vec())
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)
|
||||
.attach_printable("Could not convert secret to UTF-8")?,
|
||||
webhook_event: req_body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_headers(
|
||||
header: &actix_web::http::header::HeaderMap,
|
||||
key: &'static str,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let header_value = header
|
||||
.get(key.clone())
|
||||
.map(|value| value.to_str())
|
||||
.ok_or(errors::ConnectorError::MissingRequiredField { field_name: key })?
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::InvalidDataFormat { field_name: key })?
|
||||
.to_owned();
|
||||
Ok(header_value)
|
||||
}
|
||||
|
||||
@ -178,6 +178,80 @@ default_imp_for_complete_authorize!(
|
||||
connector::Worldpay,
|
||||
connector::Zen
|
||||
);
|
||||
macro_rules! default_imp_for_webhook_source_verification {
|
||||
($($path:ident::$connector:ident),*) => {
|
||||
$(
|
||||
impl api::ConnectorVerifyWebhookSource for $path::$connector {}
|
||||
impl
|
||||
services::ConnectorIntegration<
|
||||
api::VerifyWebhookSource,
|
||||
types::VerifyWebhookSourceRequestData,
|
||||
types::VerifyWebhookSourceResponseData,
|
||||
> for $path::$connector
|
||||
{}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
impl<const T: u8> api::ConnectorVerifyWebhookSource for connector::DummyConnector<T> {}
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
impl<const T: u8>
|
||||
services::ConnectorIntegration<
|
||||
api::VerifyWebhookSource,
|
||||
types::VerifyWebhookSourceRequestData,
|
||||
types::VerifyWebhookSourceResponseData,
|
||||
> for connector::DummyConnector<T>
|
||||
{
|
||||
}
|
||||
default_imp_for_webhook_source_verification!(
|
||||
connector::Aci,
|
||||
connector::Adyen,
|
||||
connector::Airwallex,
|
||||
connector::Authorizedotnet,
|
||||
connector::Bambora,
|
||||
connector::Bitpay,
|
||||
connector::Bluesnap,
|
||||
connector::Braintree,
|
||||
connector::Boku,
|
||||
connector::Cashtocode,
|
||||
connector::Checkout,
|
||||
connector::Coinbase,
|
||||
connector::Cryptopay,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
connector::Forte,
|
||||
connector::Globalpay,
|
||||
connector::Globepay,
|
||||
connector::Gocardless,
|
||||
connector::Helcim,
|
||||
connector::Iatapay,
|
||||
connector::Klarna,
|
||||
connector::Mollie,
|
||||
connector::Multisafepay,
|
||||
connector::Nexinets,
|
||||
connector::Nmi,
|
||||
connector::Noon,
|
||||
connector::Nuvei,
|
||||
connector::Opayo,
|
||||
connector::Opennode,
|
||||
connector::Payeezy,
|
||||
connector::Payme,
|
||||
connector::Payu,
|
||||
connector::Powertranz,
|
||||
connector::Rapyd,
|
||||
connector::Shift4,
|
||||
connector::Square,
|
||||
connector::Stax,
|
||||
connector::Stripe,
|
||||
connector::Trustpay,
|
||||
connector::Tsys,
|
||||
connector::Wise,
|
||||
connector::Worldline,
|
||||
connector::Worldpay,
|
||||
connector::Zen
|
||||
);
|
||||
|
||||
macro_rules! default_imp_for_create_customer {
|
||||
($($path:ident::$connector:ident),*) => {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use api_models::payments::HeaderPayload;
|
||||
use common_utils::errors::ReportSwitchExt;
|
||||
use error_stack::{report, IntoReport, ResultExt};
|
||||
@ -806,8 +808,40 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
|
||||
.get_webhook_object_reference_id(&request_details)
|
||||
.switch()
|
||||
.attach_printable("Could not find object reference id in incoming webhook body")?;
|
||||
let connector_enum = api_models::enums::Connector::from_str(&connector_name)
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||
field_name: "connector",
|
||||
})
|
||||
.attach_printable_lazy(|| {
|
||||
format!("unable to parse connector name {connector_name:?}")
|
||||
})?;
|
||||
let connectors_with_source_verification_call = &state.conf.webhook_source_verification_call;
|
||||
|
||||
let source_verified = connector
|
||||
let source_verified = if connectors_with_source_verification_call
|
||||
.connectors_with_webhook_source_verification_call
|
||||
.contains(&connector_enum)
|
||||
{
|
||||
connector
|
||||
.verify_webhook_source_verification_call(
|
||||
&state,
|
||||
&merchant_account,
|
||||
merchant_connector_account.clone(),
|
||||
&connector_name,
|
||||
&request_details,
|
||||
)
|
||||
.await
|
||||
.or_else(|error| match error.current_context() {
|
||||
errors::ConnectorError::WebhookSourceVerificationFailed => {
|
||||
logger::error!(?error, "Source Verification Failed");
|
||||
Ok(false)
|
||||
}
|
||||
_ => Err(error),
|
||||
})
|
||||
.switch()
|
||||
.attach_printable("There was an issue in incoming webhook source verification")?
|
||||
} else {
|
||||
connector
|
||||
.verify_webhook_source(
|
||||
&request_details,
|
||||
&merchant_account,
|
||||
@ -823,7 +857,8 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
|
||||
_ => Err(error),
|
||||
})
|
||||
.switch()
|
||||
.attach_printable("There was an issue in incoming webhook source verification")?;
|
||||
.attach_printable("There was an issue in incoming webhook source verification")?
|
||||
};
|
||||
|
||||
if source_verified {
|
||||
metrics::WEBHOOK_SOURCE_VERIFIED_COUNT.add(
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use common_utils::{errors::CustomResult, ext_traits::ValueExt};
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
errors::{self},
|
||||
payments::helpers,
|
||||
},
|
||||
db::{get_and_deserialize_key, StorageInterface},
|
||||
types::api,
|
||||
types::{self, api, domain, PaymentAddress},
|
||||
};
|
||||
|
||||
fn default_webhook_config() -> api::MerchantWebhookConfig {
|
||||
@ -13,6 +22,13 @@ fn default_webhook_config() -> api::MerchantWebhookConfig {
|
||||
])
|
||||
}
|
||||
|
||||
const IRRELEVANT_PAYMENT_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
|
||||
"irrelevant_payment_id_in_source_verification_flow";
|
||||
const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
|
||||
"irrelevant_attempt_id_in_source_verification_flow";
|
||||
const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
|
||||
"irrelevant_connector_request_reference_id_in_source_verification_flow";
|
||||
|
||||
pub async fn lookup_webhook_event(
|
||||
db: &dyn StorageInterface,
|
||||
connector_id: &str,
|
||||
@ -45,3 +61,60 @@ pub async fn lookup_webhook_event(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn construct_webhook_router_data<'a>(
|
||||
connector_name: &str,
|
||||
merchant_connector_account: domain::MerchantConnectorAccount,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
connector_wh_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
|
||||
request_details: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<types::VerifyWebhookSourceRouterData, errors::ApiErrorResponse> {
|
||||
let auth_type: types::ConnectorAuthType =
|
||||
helpers::MerchantConnectorAccountType::DbVal(merchant_connector_account.clone())
|
||||
.get_connector_account_details()
|
||||
.parse_value("ConnectorAuthType")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||
|
||||
let router_data = types::RouterData {
|
||||
flow: PhantomData,
|
||||
merchant_id: merchant_account.merchant_id.clone(),
|
||||
connector: connector_name.to_string(),
|
||||
customer_id: None,
|
||||
payment_id: IRRELEVANT_PAYMENT_ID_IN_SOURCE_VERIFICATION_FLOW.to_string(),
|
||||
attempt_id: IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW.to_string(),
|
||||
status: diesel_models::enums::AttemptStatus::default(),
|
||||
payment_method: diesel_models::enums::PaymentMethod::default(),
|
||||
connector_auth_type: auth_type,
|
||||
description: None,
|
||||
return_url: None,
|
||||
payment_method_id: None,
|
||||
address: PaymentAddress::default(),
|
||||
auth_type: diesel_models::enums::AuthenticationType::default(),
|
||||
connector_meta_data: None,
|
||||
amount_captured: None,
|
||||
request: types::VerifyWebhookSourceRequestData {
|
||||
webhook_headers: request_details.headers.clone(),
|
||||
webhook_body: request_details.body.to_vec().clone(),
|
||||
merchant_secret: connector_wh_secrets.to_owned(),
|
||||
},
|
||||
response: Err(types::ErrorResponse::default()),
|
||||
access_token: None,
|
||||
session_token: None,
|
||||
reference_id: None,
|
||||
payment_method_token: None,
|
||||
connector_customer: None,
|
||||
recurring_mandate_payment_data: None,
|
||||
preprocessing_id: None,
|
||||
connector_request_reference_id:
|
||||
IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW.to_string(),
|
||||
#[cfg(feature = "payouts")]
|
||||
payout_method_data: None,
|
||||
#[cfg(feature = "payouts")]
|
||||
quote_id: None,
|
||||
test_mode: None,
|
||||
payment_method_balance: None,
|
||||
connector_api_version: None,
|
||||
connector_http_status_code: None,
|
||||
};
|
||||
Ok(router_data)
|
||||
}
|
||||
|
||||
@ -177,6 +177,11 @@ pub type AcceptDisputeType = dyn services::ConnectorIntegration<
|
||||
AcceptDisputeRequestData,
|
||||
AcceptDisputeResponse,
|
||||
>;
|
||||
pub type VerifyWebhookSourceType = dyn services::ConnectorIntegration<
|
||||
api::VerifyWebhookSource,
|
||||
VerifyWebhookSourceRequestData,
|
||||
VerifyWebhookSourceResponseData,
|
||||
>;
|
||||
|
||||
pub type SubmitEvidenceType = dyn services::ConnectorIntegration<
|
||||
api::Evidence,
|
||||
@ -204,6 +209,12 @@ pub type VerifyRouterData = RouterData<api::Verify, VerifyRequestData, PaymentsR
|
||||
pub type AcceptDisputeRouterData =
|
||||
RouterData<api::Accept, AcceptDisputeRequestData, AcceptDisputeResponse>;
|
||||
|
||||
pub type VerifyWebhookSourceRouterData = RouterData<
|
||||
api::VerifyWebhookSource,
|
||||
VerifyWebhookSourceRequestData,
|
||||
VerifyWebhookSourceResponseData,
|
||||
>;
|
||||
|
||||
pub type SubmitEvidenceRouterData =
|
||||
RouterData<api::Evidence, SubmitEvidenceRequestData, SubmitEvidenceResponse>;
|
||||
|
||||
@ -720,6 +731,24 @@ pub enum Redirection {
|
||||
NoRedirect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerifyWebhookSourceRequestData {
|
||||
pub webhook_headers: actix_web::http::header::HeaderMap,
|
||||
pub webhook_body: Vec<u8>,
|
||||
pub merchant_secret: api_models::webhooks::ConnectorWebhookSecrets,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerifyWebhookSourceResponseData {
|
||||
pub verify_webhook_status: VerifyWebhookStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VerifyWebhookStatus {
|
||||
SourceVerified,
|
||||
SourceNotVerified,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct AcceptDisputeRequestData {
|
||||
pub dispute_id: String,
|
||||
|
||||
@ -38,6 +38,18 @@ pub trait ConnectorAccessToken:
|
||||
{
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VerifyWebhookSource;
|
||||
|
||||
pub trait ConnectorVerifyWebhookSource:
|
||||
ConnectorIntegration<
|
||||
VerifyWebhookSource,
|
||||
types::VerifyWebhookSourceRequestData,
|
||||
types::VerifyWebhookSourceResponseData,
|
||||
>
|
||||
{
|
||||
}
|
||||
|
||||
pub trait ConnectorTransactionId: ConnectorCommon + Sync {
|
||||
fn connector_transaction_id(
|
||||
&self,
|
||||
@ -123,6 +135,7 @@ pub trait Connector:
|
||||
+ FileUpload
|
||||
+ ConnectorTransactionId
|
||||
+ Payouts
|
||||
+ ConnectorVerifyWebhookSource
|
||||
{
|
||||
}
|
||||
|
||||
@ -141,7 +154,8 @@ impl<
|
||||
+ Dispute
|
||||
+ FileUpload
|
||||
+ ConnectorTransactionId
|
||||
+ Payouts,
|
||||
+ Payouts
|
||||
+ ConnectorVerifyWebhookSource,
|
||||
> Connector for T
|
||||
{
|
||||
}
|
||||
|
||||
@ -9,10 +9,14 @@ use masking::ExposeInterface;
|
||||
|
||||
use super::ConnectorCommon;
|
||||
use crate::{
|
||||
core::errors::{self, CustomResult},
|
||||
core::{
|
||||
errors::{self, CustomResult},
|
||||
payments,
|
||||
webhooks::utils::construct_webhook_router_data,
|
||||
},
|
||||
db::StorageInterface,
|
||||
services,
|
||||
types::domain,
|
||||
services::{self},
|
||||
types::{self, domain},
|
||||
utils::crypto,
|
||||
};
|
||||
|
||||
@ -139,12 +143,71 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn verify_webhook_source_verification_call(
|
||||
&self,
|
||||
state: &crate::routes::AppState,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
merchant_connector_account: domain::MerchantConnectorAccount,
|
||||
connector_name: &str,
|
||||
request_details: &IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<bool, errors::ConnectorError> {
|
||||
let connector_data = types::api::ConnectorData::get_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
connector_name,
|
||||
types::api::GetToken::Connector,
|
||||
)
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
|
||||
.attach_printable("invalid connector name received in payment attempt")?;
|
||||
let connector_integration: services::BoxedConnectorIntegration<
|
||||
'_,
|
||||
types::api::VerifyWebhookSource,
|
||||
types::VerifyWebhookSourceRequestData,
|
||||
types::VerifyWebhookSourceResponseData,
|
||||
> = connector_data.connector.get_connector_integration();
|
||||
let connector_webhook_secrets = self
|
||||
.get_webhook_source_verification_merchant_secret(
|
||||
merchant_account,
|
||||
connector_name,
|
||||
merchant_connector_account.clone(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
|
||||
|
||||
let router_data = construct_webhook_router_data(
|
||||
connector_name,
|
||||
merchant_connector_account,
|
||||
merchant_account,
|
||||
&connector_webhook_secrets,
|
||||
request_details,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
|
||||
.attach_printable("Failed while constructing webhook router data")?;
|
||||
|
||||
let response = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration,
|
||||
&router_data,
|
||||
payments::CallConnectorAction::Trigger,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let verification_result = response
|
||||
.response
|
||||
.map(|response| response.verify_webhook_status);
|
||||
match verification_result {
|
||||
Ok(types::VerifyWebhookStatus::SourceVerified) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_webhook_source(
|
||||
&self,
|
||||
request: &IncomingWebhookRequestDetails<'_>,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
merchant_connector_account: domain::MerchantConnectorAccount,
|
||||
connector_label: &str,
|
||||
connector_name: &str,
|
||||
) -> CustomResult<bool, errors::ConnectorError> {
|
||||
let algorithm = self
|
||||
.get_webhook_source_verification_algorithm(request)
|
||||
@ -156,7 +219,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
|
||||
let connector_webhook_secrets = self
|
||||
.get_webhook_source_verification_merchant_secret(
|
||||
merchant_account,
|
||||
connector_label,
|
||||
connector_name,
|
||||
merchant_connector_account,
|
||||
)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user