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:
Prasunna Soppa
2025-06-26 14:43:04 +05:30
committed by GitHub
parent 9e435929f0
commit ec6d0e4d62
25 changed files with 903 additions and 26 deletions

View File

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