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

@ -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
}))
}

View File

@ -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)]

View File

@ -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(),
))
},
)
}
}

View File

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

View File

@ -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");
}
};
};
}
}

View File

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

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,

View 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)
}

View File

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

View File

@ -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)?
}
}

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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