mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +08:00
feat(router): Add webhooks for network tokenization (#6695)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -314,11 +314,15 @@ impl SecretsHandler for settings::NetworkTokenizationService {
|
||||
let private_key = secret_management_client
|
||||
.get_secret(network_tokenization.private_key.clone())
|
||||
.await?;
|
||||
let webhook_source_verification_key = secret_management_client
|
||||
.get_secret(network_tokenization.webhook_source_verification_key.clone())
|
||||
.await?;
|
||||
|
||||
Ok(value.transition_state(|network_tokenization| Self {
|
||||
public_key,
|
||||
private_key,
|
||||
token_service_api_key,
|
||||
webhook_source_verification_key,
|
||||
..network_tokenization
|
||||
}))
|
||||
}
|
||||
|
||||
@ -601,6 +601,7 @@ pub struct NetworkTokenizationService {
|
||||
pub key_id: String,
|
||||
pub delete_token_url: url::Url,
|
||||
pub check_token_status_url: url::Url,
|
||||
pub webhook_source_verification_key: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
|
||||
@ -244,7 +244,16 @@ impl super::settings::NetworkTokenizationService {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
"private_key must not be empty".into(),
|
||||
))
|
||||
})
|
||||
})?;
|
||||
|
||||
when(
|
||||
self.webhook_source_verification_key.is_default_or_empty(),
|
||||
|| {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
"webhook_source_verification_key must not be empty".into(),
|
||||
))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,16 +20,16 @@ use api_models::payment_methods;
|
||||
#[cfg(feature = "payouts")]
|
||||
pub use api_models::{enums::PayoutConnectors, payouts as payout_types};
|
||||
#[cfg(feature = "v1")]
|
||||
use common_utils::ext_traits::{Encode, OptionExt};
|
||||
use common_utils::{consts::DEFAULT_LOCALE, id_type};
|
||||
use common_utils::{consts::DEFAULT_LOCALE, ext_traits::OptionExt};
|
||||
#[cfg(feature = "v2")]
|
||||
use common_utils::{
|
||||
crypto::Encryptable,
|
||||
errors::CustomResult,
|
||||
ext_traits::{AsyncExt, Encode, ValueExt},
|
||||
ext_traits::{AsyncExt, ValueExt},
|
||||
fp_utils::when,
|
||||
generate_id, types as util_types,
|
||||
};
|
||||
use common_utils::{ext_traits::Encode, id_type};
|
||||
use diesel_models::{
|
||||
enums, GenericLinkNew, PaymentMethodCollectLink, PaymentMethodCollectLinkData,
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ use api_models::{
|
||||
payment_methods::PaymentMethodDataWalletInfo, payments::ConnectorMandateReferenceId,
|
||||
};
|
||||
use common_enums::{ConnectorMandateStatus, PaymentMethod};
|
||||
use common_types::callback_mapper::CallbackMapperData;
|
||||
use common_utils::{
|
||||
crypto::Encryptable,
|
||||
ext_traits::{AsyncExt, Encode, ValueExt},
|
||||
@ -17,6 +18,7 @@ use common_utils::{
|
||||
use error_stack::{report, ResultExt};
|
||||
#[cfg(feature = "v1")]
|
||||
use hyperswitch_domain_models::{
|
||||
callback_mapper::CallbackMapper,
|
||||
mandates::{CommonMandateReference, PaymentsMandateReference, PaymentsMandateReferenceRecord},
|
||||
payment_method_data,
|
||||
};
|
||||
@ -754,11 +756,45 @@ where
|
||||
card.card_network
|
||||
.map(|card_network| card_network.to_string())
|
||||
}),
|
||||
network_token_requestor_ref_id,
|
||||
network_token_requestor_ref_id.clone(),
|
||||
network_token_locker_id,
|
||||
pm_network_token_data_encrypted,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match network_token_requestor_ref_id {
|
||||
Some(network_token_requestor_ref_id) => {
|
||||
//Insert the network token reference ID along with merchant id, customer id in CallbackMapper table for its respective webooks
|
||||
let callback_mapper_data =
|
||||
CallbackMapperData::NetworkTokenWebhook {
|
||||
merchant_id: merchant_context
|
||||
.get_merchant_account()
|
||||
.get_id()
|
||||
.clone(),
|
||||
customer_id,
|
||||
payment_method_id: resp.payment_method_id.clone(),
|
||||
};
|
||||
let callback_mapper = CallbackMapper::new(
|
||||
network_token_requestor_ref_id,
|
||||
common_enums::CallbackMapperIdType::NetworkTokenRequestorRefernceID,
|
||||
callback_mapper_data,
|
||||
common_utils::date_time::now(),
|
||||
common_utils::date_time::now(),
|
||||
);
|
||||
|
||||
db.insert_call_back_mapper(callback_mapper)
|
||||
.await
|
||||
.change_context(
|
||||
errors::ApiErrorResponse::InternalServerError,
|
||||
)
|
||||
.attach_printable(
|
||||
"Failed to insert in Callback Mapper table",
|
||||
)?;
|
||||
}
|
||||
None => {
|
||||
logger::info!("Network token requestor reference ID is not available, skipping callback mapper insertion");
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ mod incoming;
|
||||
#[cfg(feature = "v2")]
|
||||
mod incoming_v2;
|
||||
#[cfg(feature = "v1")]
|
||||
mod network_tokenization_incoming;
|
||||
#[cfg(feature = "v1")]
|
||||
mod outgoing;
|
||||
#[cfg(feature = "v2")]
|
||||
mod outgoing_v2;
|
||||
@ -15,7 +17,7 @@ pub mod webhook_events;
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
pub(crate) use self::{
|
||||
incoming::incoming_webhooks_wrapper,
|
||||
incoming::{incoming_webhooks_wrapper, network_token_incoming_webhooks_wrapper},
|
||||
outgoing::{
|
||||
create_event_and_trigger_outgoing_webhook, get_outgoing_webhook_request,
|
||||
trigger_webhook_and_raise_event,
|
||||
|
||||
@ -7,7 +7,7 @@ use api_models::webhooks::{self, WebhookResponseTracker};
|
||||
use common_utils::{
|
||||
errors::ReportSwitchExt,
|
||||
events::ApiEventsType,
|
||||
ext_traits::AsyncExt,
|
||||
ext_traits::{AsyncExt, ByteSliceExt},
|
||||
types::{AmountConvertor, StringMinorUnitForConnector},
|
||||
};
|
||||
use diesel_models::{refund as diesel_refund, ConnectorMandateReferenceId};
|
||||
@ -28,10 +28,10 @@ use crate::{
|
||||
core::{
|
||||
api_locking,
|
||||
errors::{self, ConnectorErrorExt, CustomResult, RouterResponse, StorageErrorExt},
|
||||
metrics,
|
||||
metrics, payment_methods,
|
||||
payments::{self, tokenization},
|
||||
refunds, relay, utils as core_utils,
|
||||
webhooks::utils::construct_webhook_router_data,
|
||||
webhooks::{network_tokenization_incoming, utils::construct_webhook_router_data},
|
||||
},
|
||||
db::StorageInterface,
|
||||
events::api_logs::ApiEvent,
|
||||
@ -125,6 +125,68 @@ pub async fn incoming_webhooks_wrapper<W: types::OutgoingWebhookType>(
|
||||
Ok(application_response)
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
pub async fn network_token_incoming_webhooks_wrapper<W: types::OutgoingWebhookType>(
|
||||
flow: &impl router_env::types::FlowMetric,
|
||||
state: SessionState,
|
||||
req: &actix_web::HttpRequest,
|
||||
body: actix_web::web::Bytes,
|
||||
) -> RouterResponse<serde_json::Value> {
|
||||
let start_instant = Instant::now();
|
||||
|
||||
let request_details: IncomingWebhookRequestDetails<'_> = IncomingWebhookRequestDetails {
|
||||
method: req.method().clone(),
|
||||
uri: req.uri().clone(),
|
||||
headers: req.headers(),
|
||||
query_params: req.query_string().to_string(),
|
||||
body: &body,
|
||||
};
|
||||
|
||||
let (application_response, webhooks_response_tracker, serialized_req, merchant_id) = Box::pin(
|
||||
network_token_incoming_webhooks_core::<W>(&state, request_details),
|
||||
)
|
||||
.await?;
|
||||
|
||||
logger::info!(incoming_webhook_payload = ?serialized_req);
|
||||
|
||||
let request_duration = Instant::now()
|
||||
.saturating_duration_since(start_instant)
|
||||
.as_millis();
|
||||
|
||||
let request_id = RequestId::extract(req)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Unable to extract request id from request")?;
|
||||
let auth_type = auth::AuthenticationType::NoAuth;
|
||||
let status_code = 200;
|
||||
let api_event = ApiEventsType::NetworkTokenWebhook {
|
||||
payment_method_id: webhooks_response_tracker.get_payment_method_id(),
|
||||
};
|
||||
let response_value = serde_json::to_value(&webhooks_response_tracker)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not convert webhook effect to string")?;
|
||||
let infra = state.infra_components.clone();
|
||||
let api_event = ApiEvent::new(
|
||||
state.tenant.tenant_id.clone(),
|
||||
Some(merchant_id),
|
||||
flow,
|
||||
&request_id,
|
||||
request_duration,
|
||||
status_code,
|
||||
serialized_req,
|
||||
Some(response_value),
|
||||
None,
|
||||
auth_type,
|
||||
None,
|
||||
api_event,
|
||||
req,
|
||||
req.method(),
|
||||
infra,
|
||||
);
|
||||
state.event_handler().log_event(&api_event);
|
||||
Ok(application_response)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(skip_all)]
|
||||
async fn incoming_webhooks_core<W: types::OutgoingWebhookType>(
|
||||
@ -598,6 +660,81 @@ fn handle_incoming_webhook_error(
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
#[cfg(feature = "v1")]
|
||||
async fn network_token_incoming_webhooks_core<W: types::OutgoingWebhookType>(
|
||||
state: &SessionState,
|
||||
request_details: IncomingWebhookRequestDetails<'_>,
|
||||
) -> errors::RouterResult<(
|
||||
services::ApplicationResponse<serde_json::Value>,
|
||||
WebhookResponseTracker,
|
||||
serde_json::Value,
|
||||
common_utils::id_type::MerchantId,
|
||||
)> {
|
||||
let serialized_request =
|
||||
network_tokenization_incoming::get_network_token_resource_object(&request_details)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Network Token Requestor Webhook deserialization failed")?
|
||||
.masked_serialize()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Could not convert webhook effect to string")?;
|
||||
|
||||
let network_tokenization_service = &state
|
||||
.conf
|
||||
.network_tokenization_service
|
||||
.as_ref()
|
||||
.ok_or(errors::NetworkTokenizationError::NetworkTokenizationServiceNotConfigured)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Network Tokenization Service not configured")?;
|
||||
|
||||
//source verification
|
||||
network_tokenization_incoming::Authorization::new(request_details.headers.get("Authorization"))
|
||||
.verify_webhook_source(network_tokenization_service.get_inner())
|
||||
.await?;
|
||||
|
||||
let response: network_tokenization_incoming::NetworkTokenWebhookResponse = request_details
|
||||
.body
|
||||
.parse_struct("NetworkTokenWebhookResponse")
|
||||
.change_context(errors::ApiErrorResponse::WebhookUnprocessableEntity)?;
|
||||
|
||||
let (merchant_id, payment_method_id, _customer_id) = response
|
||||
.fetch_merchant_id_payment_method_id_customer_id_from_callback_mapper(state)
|
||||
.await?;
|
||||
|
||||
metrics::WEBHOOK_SOURCE_VERIFIED_COUNT.add(
|
||||
1,
|
||||
router_env::metric_attributes!((MERCHANT_ID, merchant_id.clone())),
|
||||
);
|
||||
|
||||
let merchant_context =
|
||||
network_tokenization_incoming::fetch_merchant_account_for_network_token_webhooks(
|
||||
state,
|
||||
&merchant_id,
|
||||
)
|
||||
.await?;
|
||||
let payment_method =
|
||||
network_tokenization_incoming::fetch_payment_method_for_network_token_webhooks(
|
||||
state,
|
||||
merchant_context.get_merchant_account(),
|
||||
merchant_context.get_merchant_key_store(),
|
||||
&payment_method_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response_data = response.get_response_data();
|
||||
|
||||
let webhook_resp_tracker = response_data
|
||||
.update_payment_method(state, &payment_method, &merchant_context)
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
services::ApplicationResponse::StatusOk,
|
||||
webhook_resp_tracker,
|
||||
serialized_request,
|
||||
merchant_id.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(skip_all)]
|
||||
async fn payments_incoming_webhook_flow(
|
||||
@ -1316,7 +1453,7 @@ async fn external_authentication_incoming_webhook_flow(
|
||||
authentication_details
|
||||
.authentication_value
|
||||
.async_map(|auth_val| {
|
||||
crate::core::payment_methods::vault::create_tokenize(
|
||||
payment_methods::vault::create_tokenize(
|
||||
&state,
|
||||
auth_val.expose(),
|
||||
None,
|
||||
|
||||
463
crates/router/src/core/webhooks/network_tokenization_incoming.rs
Normal file
463
crates/router/src/core/webhooks/network_tokenization_incoming.rs
Normal file
@ -0,0 +1,463 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use ::payment_methods::controller::PaymentMethodsController;
|
||||
use api_models::webhooks::WebhookResponseTracker;
|
||||
use async_trait::async_trait;
|
||||
use common_utils::{
|
||||
crypto::Encryptable,
|
||||
ext_traits::{AsyncExt, ByteSliceExt, ValueExt},
|
||||
id_type,
|
||||
};
|
||||
use error_stack::{report, ResultExt};
|
||||
use http::HeaderValue;
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::{
|
||||
errors::{self, CustomResult, RouterResult, StorageErrorExt},
|
||||
payment_methods::cards,
|
||||
},
|
||||
logger,
|
||||
routes::{app::SessionStateInfo, SessionState},
|
||||
types::{
|
||||
api, domain, payment_methods as pm_types,
|
||||
storage::{self, enums},
|
||||
},
|
||||
utils::{self as helper_utils, ext_traits::OptionExt},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum NetworkTokenWebhookResponse {
|
||||
PanMetadataUpdate(pm_types::PanMetadataUpdateBody),
|
||||
NetworkTokenMetadataUpdate(pm_types::NetworkTokenMetaDataUpdateBody),
|
||||
}
|
||||
|
||||
impl NetworkTokenWebhookResponse {
|
||||
fn get_network_token_requestor_ref_id(&self) -> String {
|
||||
match self {
|
||||
Self::PanMetadataUpdate(data) => data.card.card_reference.clone(),
|
||||
Self::NetworkTokenMetadataUpdate(data) => data.token.card_reference.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_response_data(self) -> Box<dyn NetworkTokenWebhookResponseExt> {
|
||||
match self {
|
||||
Self::PanMetadataUpdate(data) => Box::new(data),
|
||||
Self::NetworkTokenMetadataUpdate(data) => Box::new(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_merchant_id_payment_method_id_customer_id_from_callback_mapper(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
) -> RouterResult<(id_type::MerchantId, String, id_type::CustomerId)> {
|
||||
let network_token_requestor_ref_id = &self.get_network_token_requestor_ref_id();
|
||||
|
||||
let db = &*state.store;
|
||||
let callback_mapper_data = db
|
||||
.find_call_back_mapper_by_id(network_token_requestor_ref_id)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to fetch callback mapper data")?;
|
||||
|
||||
Ok(callback_mapper_data
|
||||
.data
|
||||
.get_network_token_webhook_details())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_network_token_resource_object(
|
||||
request_details: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::NetworkTokenizationError> {
|
||||
let response: NetworkTokenWebhookResponse = request_details
|
||||
.body
|
||||
.parse_struct("NetworkTokenWebhookResponse")
|
||||
.change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?;
|
||||
Ok(Box::new(response))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait NetworkTokenWebhookResponseExt {
|
||||
fn decrypt_payment_method_data(
|
||||
&self,
|
||||
payment_method: &domain::PaymentMethod,
|
||||
) -> CustomResult<api::payment_methods::CardDetailFromLocker, errors::ApiErrorResponse>;
|
||||
|
||||
async fn update_payment_method(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
payment_method: &domain::PaymentMethod,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NetworkTokenWebhookResponseExt for pm_types::PanMetadataUpdateBody {
|
||||
fn decrypt_payment_method_data(
|
||||
&self,
|
||||
payment_method: &domain::PaymentMethod,
|
||||
) -> CustomResult<api::payment_methods::CardDetailFromLocker, errors::ApiErrorResponse> {
|
||||
let decrypted_data = payment_method
|
||||
.payment_method_data
|
||||
.clone()
|
||||
.map(|payment_method_data| payment_method_data.into_inner().expose())
|
||||
.and_then(|val| {
|
||||
val.parse_value::<api::payment_methods::PaymentMethodsData>("PaymentMethodsData")
|
||||
.map_err(|err| logger::error!(?err, "Failed to parse PaymentMethodsData"))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|pmd| match pmd {
|
||||
api::payment_methods::PaymentMethodsData::Card(token) => {
|
||||
Some(api::payment_methods::CardDetailFromLocker::from(token))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to obtain decrypted token object from db")?;
|
||||
Ok(decrypted_data)
|
||||
}
|
||||
|
||||
async fn update_payment_method(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
payment_method: &domain::PaymentMethod,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
|
||||
let decrypted_data = self.decrypt_payment_method_data(payment_method)?;
|
||||
handle_metadata_update(
|
||||
state,
|
||||
&self.card,
|
||||
payment_method
|
||||
.locker_id
|
||||
.clone()
|
||||
.get_required_value("locker_id")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Locker id is not found for the payment method")?,
|
||||
payment_method,
|
||||
merchant_context,
|
||||
decrypted_data,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NetworkTokenWebhookResponseExt for pm_types::NetworkTokenMetaDataUpdateBody {
|
||||
fn decrypt_payment_method_data(
|
||||
&self,
|
||||
payment_method: &domain::PaymentMethod,
|
||||
) -> CustomResult<api::payment_methods::CardDetailFromLocker, errors::ApiErrorResponse> {
|
||||
let decrypted_data = payment_method
|
||||
.network_token_payment_method_data
|
||||
.clone()
|
||||
.map(|x| x.into_inner().expose())
|
||||
.and_then(|val| {
|
||||
val.parse_value::<api::payment_methods::PaymentMethodsData>("PaymentMethodsData")
|
||||
.map_err(|err| logger::error!(?err, "Failed to parse PaymentMethodsData"))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|pmd| match pmd {
|
||||
api::payment_methods::PaymentMethodsData::Card(token) => {
|
||||
Some(api::payment_methods::CardDetailFromLocker::from(token))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to obtain decrypted token object from db")?;
|
||||
Ok(decrypted_data)
|
||||
}
|
||||
|
||||
async fn update_payment_method(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
payment_method: &domain::PaymentMethod,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
|
||||
let decrypted_data = self.decrypt_payment_method_data(payment_method)?;
|
||||
handle_metadata_update(
|
||||
state,
|
||||
&self.token,
|
||||
payment_method
|
||||
.network_token_locker_id
|
||||
.clone()
|
||||
.get_required_value("locker_id")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Locker id is not found for the payment method")?,
|
||||
payment_method,
|
||||
merchant_context,
|
||||
decrypted_data,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Authorization {
|
||||
header: Option<HeaderValue>,
|
||||
}
|
||||
|
||||
impl Authorization {
|
||||
pub fn new(header: Option<&HeaderValue>) -> Self {
|
||||
Self {
|
||||
header: header.cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_webhook_source(
|
||||
self,
|
||||
nt_service: &settings::NetworkTokenizationService,
|
||||
) -> CustomResult<(), errors::ApiErrorResponse> {
|
||||
let secret = nt_service.webhook_source_verification_key.clone();
|
||||
|
||||
let source_verified = match self.header {
|
||||
Some(authorization_header) => match authorization_header.to_str() {
|
||||
Ok(header_value) => Ok(header_value == secret.expose()),
|
||||
Err(err) => {
|
||||
logger::error!(?err, "Failed to parse authorization header");
|
||||
Err(errors::ApiErrorResponse::WebhookAuthenticationFailed)
|
||||
}
|
||||
},
|
||||
None => Ok(false),
|
||||
}?;
|
||||
logger::info!(source_verified=?source_verified);
|
||||
|
||||
helper_utils::when(!source_verified, || {
|
||||
Err(report!(
|
||||
errors::ApiErrorResponse::WebhookAuthenticationFailed
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn handle_metadata_update(
|
||||
state: &SessionState,
|
||||
metadata: &pm_types::NetworkTokenRequestorData,
|
||||
locker_id: String,
|
||||
payment_method: &domain::PaymentMethod,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
decrypted_data: api::payment_methods::CardDetailFromLocker,
|
||||
is_pan_update: bool,
|
||||
) -> RouterResult<WebhookResponseTracker> {
|
||||
let merchant_id = merchant_context.get_merchant_account().get_id();
|
||||
let customer_id = &payment_method.customer_id;
|
||||
let payment_method_id = payment_method.get_id().clone();
|
||||
let status = payment_method.status;
|
||||
|
||||
match metadata.is_update_required(decrypted_data) {
|
||||
false => {
|
||||
logger::info!(
|
||||
"No update required for payment method {} for locker_id {}",
|
||||
payment_method.get_id(),
|
||||
locker_id
|
||||
);
|
||||
Ok(WebhookResponseTracker::PaymentMethod {
|
||||
payment_method_id,
|
||||
status,
|
||||
})
|
||||
}
|
||||
true => {
|
||||
let mut card = cards::get_card_from_locker(state, customer_id, merchant_id, &locker_id)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to fetch token information from the locker")?;
|
||||
|
||||
card.card_exp_year = metadata.expiry_year.clone();
|
||||
card.card_exp_month = metadata.expiry_month.clone();
|
||||
|
||||
let card_network = card
|
||||
.card_brand
|
||||
.clone()
|
||||
.map(|card_brand| enums::CardNetwork::from_str(&card_brand))
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||
field_name: "card network",
|
||||
})
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Invalid Card Network stored in vault")?;
|
||||
|
||||
let card_data = api::payment_methods::CardDetail::from((card, card_network));
|
||||
|
||||
let payment_method_request: api::payment_methods::PaymentMethodCreate =
|
||||
PaymentMethodCreateWrapper::from((&card_data, payment_method)).get_inner();
|
||||
|
||||
let pm_cards = cards::PmCards {
|
||||
state,
|
||||
merchant_context,
|
||||
};
|
||||
|
||||
pm_cards
|
||||
.delete_card_from_locker(customer_id, merchant_id, &locker_id)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to delete network token")?;
|
||||
|
||||
let (res, _) = pm_cards
|
||||
.add_card_to_locker(payment_method_request, &card_data, customer_id, None)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to add network token")?;
|
||||
|
||||
let pm_details = res.card.as_ref().map(|card| {
|
||||
api::payment_methods::PaymentMethodsData::Card(
|
||||
api::payment_methods::CardDetailsPaymentMethod::from((card.clone(), None)),
|
||||
)
|
||||
});
|
||||
let key_manager_state = state.into();
|
||||
|
||||
let pm_data_encrypted: Option<Encryptable<Secret<serde_json::Value>>> = pm_details
|
||||
.async_map(|pm_card| {
|
||||
cards::create_encrypted_data(
|
||||
&key_manager_state,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
pm_card,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Unable to encrypt payment method data")?;
|
||||
|
||||
let pm_update = if is_pan_update {
|
||||
storage::PaymentMethodUpdate::AdditionalDataUpdate {
|
||||
locker_id: Some(res.payment_method_id),
|
||||
payment_method_data: pm_data_encrypted.map(Into::into),
|
||||
status: None,
|
||||
payment_method: None,
|
||||
payment_method_type: None,
|
||||
payment_method_issuer: None,
|
||||
network_token_requestor_reference_id: None,
|
||||
network_token_locker_id: None,
|
||||
network_token_payment_method_data: None,
|
||||
}
|
||||
} else {
|
||||
storage::PaymentMethodUpdate::AdditionalDataUpdate {
|
||||
locker_id: None,
|
||||
payment_method_data: None,
|
||||
status: None,
|
||||
payment_method: None,
|
||||
payment_method_type: None,
|
||||
payment_method_issuer: None,
|
||||
network_token_requestor_reference_id: None,
|
||||
network_token_locker_id: Some(res.payment_method_id),
|
||||
network_token_payment_method_data: pm_data_encrypted.map(Into::into),
|
||||
}
|
||||
};
|
||||
let db = &*state.store;
|
||||
|
||||
db.update_payment_method(
|
||||
&key_manager_state,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
payment_method.clone(),
|
||||
pm_update,
|
||||
merchant_context.get_merchant_account().storage_scheme,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to update the payment method")?;
|
||||
|
||||
Ok(WebhookResponseTracker::PaymentMethod {
|
||||
payment_method_id,
|
||||
status,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PaymentMethodCreateWrapper(pub api::payment_methods::PaymentMethodCreate);
|
||||
|
||||
impl From<(&api::payment_methods::CardDetail, &domain::PaymentMethod)>
|
||||
for PaymentMethodCreateWrapper
|
||||
{
|
||||
fn from(
|
||||
(data, payment_method): (&api::payment_methods::CardDetail, &domain::PaymentMethod),
|
||||
) -> Self {
|
||||
Self(api::payment_methods::PaymentMethodCreate {
|
||||
customer_id: Some(payment_method.customer_id.clone()),
|
||||
payment_method: payment_method.payment_method,
|
||||
payment_method_type: payment_method.payment_method_type,
|
||||
payment_method_issuer: payment_method.payment_method_issuer.clone(),
|
||||
payment_method_issuer_code: payment_method.payment_method_issuer_code,
|
||||
metadata: payment_method.metadata.clone(),
|
||||
payment_method_data: None,
|
||||
connector_mandate_details: None,
|
||||
client_secret: None,
|
||||
billing: None,
|
||||
card: Some(data.clone()),
|
||||
card_network: data
|
||||
.card_network
|
||||
.clone()
|
||||
.map(|card_network| card_network.to_string()),
|
||||
bank_transfer: None,
|
||||
wallet: None,
|
||||
network_transaction_id: payment_method.network_transaction_id.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PaymentMethodCreateWrapper {
|
||||
fn get_inner(self) -> api::payment_methods::PaymentMethodCreate {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_merchant_account_for_network_token_webhooks(
|
||||
state: &SessionState,
|
||||
merchant_id: &id_type::MerchantId,
|
||||
) -> RouterResult<domain::MerchantContext> {
|
||||
let db = &*state.store;
|
||||
let key_manager_state = &(state).into();
|
||||
|
||||
let key_store = state
|
||||
.store()
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
key_manager_state,
|
||||
merchant_id,
|
||||
&state.store().get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::Unauthorized)
|
||||
.attach_printable("Failed to fetch merchant key store for the merchant id")?;
|
||||
|
||||
let merchant_account = db
|
||||
.find_merchant_account_by_merchant_id(key_manager_state, merchant_id, &key_store)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to fetch merchant account for the merchant id")?;
|
||||
|
||||
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(domain::Context(
|
||||
merchant_account.clone(),
|
||||
key_store,
|
||||
)));
|
||||
|
||||
Ok(merchant_context)
|
||||
}
|
||||
|
||||
pub async fn fetch_payment_method_for_network_token_webhooks(
|
||||
state: &SessionState,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
payment_method_id: &str,
|
||||
) -> RouterResult<domain::PaymentMethod> {
|
||||
let db = &*state.store;
|
||||
let key_manager_state = &(state).into();
|
||||
|
||||
let payment_method = db
|
||||
.find_payment_method(
|
||||
key_manager_state,
|
||||
key_store,
|
||||
payment_method_id,
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)
|
||||
.attach_printable("Failed to fetch the payment method")?;
|
||||
|
||||
Ok(payment_method)
|
||||
}
|
||||
@ -142,6 +142,7 @@ pub trait StorageInterface:
|
||||
+ user::theme::ThemeInterface
|
||||
+ payment_method_session::PaymentMethodsSessionInterface
|
||||
+ tokenization::TokenizationInterface
|
||||
+ callback_mapper::CallbackMapperInterface
|
||||
+ 'static
|
||||
{
|
||||
fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use error_stack::report;
|
||||
use hyperswitch_domain_models::callback_mapper as domain;
|
||||
use router_env::{instrument, tracing};
|
||||
use storage_impl::DataModelExt;
|
||||
use storage_impl::{DataModelExt, MockDb};
|
||||
|
||||
use super::Store;
|
||||
use crate::{
|
||||
@ -51,3 +51,22 @@ impl CallbackMapperInterface for Store {
|
||||
.map(domain::CallbackMapper::from_storage_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CallbackMapperInterface for MockDb {
|
||||
#[instrument(skip_all)]
|
||||
async fn insert_call_back_mapper(
|
||||
&self,
|
||||
_call_back_mapper: domain::CallbackMapper,
|
||||
) -> CustomResult<domain::CallbackMapper, errors::StorageError> {
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn find_call_back_mapper_by_id(
|
||||
&self,
|
||||
_id: &str,
|
||||
) -> CustomResult<domain::CallbackMapper, errors::StorageError> {
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
}
|
||||
|
||||
@ -1710,6 +1710,24 @@ impl Webhooks {
|
||||
#[allow(unused_mut)]
|
||||
let mut route = web::scope("/webhooks")
|
||||
.app_data(web::Data::new(config))
|
||||
.service(
|
||||
web::resource("/network_token_requestor/ref")
|
||||
.route(
|
||||
web::post().to(receive_network_token_requestor_incoming_webhook::<
|
||||
webhook_type::OutgoingWebhook,
|
||||
>),
|
||||
)
|
||||
.route(
|
||||
web::get().to(receive_network_token_requestor_incoming_webhook::<
|
||||
webhook_type::OutgoingWebhook,
|
||||
>),
|
||||
)
|
||||
.route(
|
||||
web::put().to(receive_network_token_requestor_incoming_webhook::<
|
||||
webhook_type::OutgoingWebhook,
|
||||
>),
|
||||
),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{merchant_id}/{connector_id_or_name}")
|
||||
.route(
|
||||
|
||||
@ -192,7 +192,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::WebhookEventInitialDeliveryAttemptList
|
||||
| Flow::WebhookEventDeliveryAttemptList
|
||||
| Flow::WebhookEventDeliveryRetry
|
||||
| Flow::RecoveryIncomingWebhookReceive => Self::Webhooks,
|
||||
| Flow::RecoveryIncomingWebhookReceive
|
||||
| Flow::IncomingNetworkTokenWebhookReceive => Self::Webhooks,
|
||||
|
||||
Flow::ApiKeyCreate
|
||||
| Flow::ApiKeyRetrieve
|
||||
|
||||
@ -179,3 +179,32 @@ pub async fn receive_incoming_webhook<W: types::OutgoingWebhookType>(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::IncomingNetworkTokenWebhookReceive))]
|
||||
pub async fn receive_network_token_requestor_incoming_webhook<W: types::OutgoingWebhookType>(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
body: web::Bytes,
|
||||
_path: web::Path<String>,
|
||||
) -> impl Responder {
|
||||
let flow = Flow::IncomingNetworkTokenWebhookReceive;
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow.clone(),
|
||||
state,
|
||||
&req,
|
||||
(),
|
||||
|state, _: (), _, _| {
|
||||
webhooks::network_token_incoming_webhooks_wrapper::<W>(
|
||||
&flow,
|
||||
state.to_owned(),
|
||||
&req,
|
||||
body.clone(),
|
||||
)
|
||||
},
|
||||
&auth::NoAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ mod customers {
|
||||
pub use hyperswitch_domain_models::customer::*;
|
||||
}
|
||||
|
||||
mod callback_mapper {
|
||||
pub mod callback_mapper {
|
||||
pub use hyperswitch_domain_models::callback_mapper::CallbackMapper;
|
||||
}
|
||||
|
||||
|
||||
@ -15,10 +15,11 @@ use hyperswitch_domain_models::payment_method_data::NetworkTokenDetails;
|
||||
use masking::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::api;
|
||||
#[cfg(feature = "v2")]
|
||||
use crate::{
|
||||
consts,
|
||||
types::{api, domain, storage},
|
||||
types::{domain, storage},
|
||||
};
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
@ -357,3 +358,33 @@ pub struct CheckTokenStatusResponsePayload {
|
||||
pub struct CheckTokenStatusResponse {
|
||||
pub payload: CheckTokenStatusResponsePayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct NetworkTokenRequestorData {
|
||||
pub card_reference: String,
|
||||
pub customer_id: String,
|
||||
pub expiry_year: Secret<String>,
|
||||
pub expiry_month: Secret<String>,
|
||||
}
|
||||
|
||||
impl NetworkTokenRequestorData {
|
||||
pub fn is_update_required(
|
||||
&self,
|
||||
data_stored_in_vault: api::payment_methods::CardDetailFromLocker,
|
||||
) -> bool {
|
||||
//if the expiry year and month in the vault are not the same as the ones in the requestor data,
|
||||
//then we need to update the vault data with the updated expiry year and month.
|
||||
!((data_stored_in_vault.expiry_year.unwrap_or_default() == self.expiry_year)
|
||||
&& (data_stored_in_vault.expiry_month.unwrap_or_default() == self.expiry_month))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct NetworkTokenMetaDataUpdateBody {
|
||||
pub token: NetworkTokenRequestorData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PanMetadataUpdateBody {
|
||||
pub card: NetworkTokenRequestorData,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user