feat(core): Added integrity framework for Authorize and Sync flow with connector as Stripe (#5109)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com>
Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in>
This commit is contained in:
Sahkal Poddar
2024-07-04 15:06:06 +05:30
committed by GitHub
parent 0796bb3b25
commit c8c0cb765e
37 changed files with 725 additions and 234 deletions

View File

@ -181,6 +181,7 @@ pub fn construct_router_data<F: Clone, Req, Res>(
refund_id: None,
payment_method_status: None,
connector_response: None,
integrity_check: Ok(()),
})
}

View File

@ -130,6 +130,7 @@ impl ConstructFlowSpecificData<frm_api::Checkout, FraudCheckCheckoutData, FraudC
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -111,6 +111,7 @@ pub async fn construct_fulfillment_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}

View File

@ -102,6 +102,7 @@ impl ConstructFlowSpecificData<RecordReturn, FraudCheckRecordReturnData, FraudCh
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -111,6 +111,7 @@ impl ConstructFlowSpecificData<frm_api::Sale, FraudCheckSaleData, FraudCheckResp
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -115,6 +115,7 @@ impl
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -75,6 +75,7 @@ pub async fn construct_mandate_revoke_router_data(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -76,7 +76,7 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
if self.should_proceed_with_authorize() {
self.decide_authentication_type();
logger::debug!(auth_type=?self.auth_type);
let resp = services::execute_connector_processing_step(
let mut new_router_data = services::execute_connector_processing_step(
state,
connector_integration,
&self,
@ -86,8 +86,16 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
.await
.to_payment_failed_response()?;
// Initiating Integrity check
let integrity_result = helpers::check_integrity_based_on_flow(
&new_router_data.request,
&new_router_data.response,
);
new_router_data.integrity_check = integrity_result;
metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics
Ok(resp)
Ok(new_router_data)
} else {
Ok(self.clone())
}

View File

@ -85,7 +85,7 @@ impl Feature<api::PSync, types::PaymentsSyncData>
(types::SyncRequestType::MultipleCaptureSync(_), Err(err)) => Err(err),
_ => {
// for bulk sync of captures, above logic needs to be handled at connector end
let resp = services::execute_connector_processing_step(
let mut new_router_data = services::execute_connector_processing_step(
state,
connector_integration,
&self,
@ -94,7 +94,16 @@ impl Feature<api::PSync, types::PaymentsSyncData>
)
.await
.to_payment_failed_response()?;
Ok(resp)
// Initiating Integrity checks
let integrity_result = helpers::check_integrity_based_on_flow(
&new_router_data.request,
&new_router_data.response,
);
new_router_data.integrity_check = integrity_result;
Ok(new_router_data)
}
}
}

View File

@ -21,6 +21,7 @@ use hyperswitch_domain_models::{
payments::{payment_attempt::PaymentAttempt, payment_intent::CustomerData, PaymentIntent},
router_data::KlarnaSdkResponse,
};
use hyperswitch_interfaces::integrity::{CheckIntegrity, FlowIntegrity, GetIntegrityObject};
use josekit::jwe;
use masking::{ExposeInterface, PeekInterface};
use openssl::{
@ -66,7 +67,7 @@ use crate::{
},
transformers::{ForeignFrom, ForeignTryFrom},
AdditionalPaymentMethodConnectorResponse, ErrorResponse, MandateReference,
RecurringMandatePaymentData, RouterData,
PaymentsResponseData, RecurringMandatePaymentData, RouterData,
},
utils::{
self,
@ -3523,6 +3524,7 @@ pub fn router_data_type_conversion<F1, F2, Req1, Req2, Res1, Res2>(
refund_id: router_data.refund_id,
dispute_id: router_data.dispute_id,
connector_response: router_data.connector_response,
integrity_check: Ok(()),
connector_wallets_details: router_data.connector_wallets_details,
}
}
@ -4946,6 +4948,35 @@ pub fn get_redis_key_for_extended_card_info(merchant_id: &str, payment_id: &str)
format!("{merchant_id}_{payment_id}_extended_card_info")
}
pub fn check_integrity_based_on_flow<T, Request>(
request: &Request,
payment_response_data: &Result<PaymentsResponseData, ErrorResponse>,
) -> Result<(), common_utils::errors::IntegrityCheckError>
where
T: FlowIntegrity,
Request: GetIntegrityObject<T> + CheckIntegrity<Request, T>,
{
let connector_transaction_id = match payment_response_data {
Ok(resp_data) => match resp_data {
PaymentsResponseData::TransactionResponse {
connector_response_reference_id,
..
} => connector_response_reference_id,
PaymentsResponseData::TransactionUnresolvedResponse {
connector_response_reference_id,
..
} => connector_response_reference_id,
PaymentsResponseData::PreProcessingResponse {
connector_response_reference_id,
..
} => connector_response_reference_id,
_ => &None,
},
Err(_) => &None,
};
request.check_integrity(request, connector_transaction_id.to_owned())
}
pub async fn config_skip_saving_wallet_at_connector(
db: &dyn StorageInterface,
merchant_id: &String,

View File

@ -7,7 +7,7 @@ use error_stack::{report, ResultExt};
use futures::FutureExt;
use hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt;
use router_derive;
use router_env::{instrument, logger, tracing};
use router_env::{instrument, logger, metrics::add_attributes, tracing};
use storage_impl::DataModelExt;
use tracing_futures::Instrument;
@ -868,94 +868,11 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
};
(capture_update, attempt_update)
}
Ok(payments_response) => {
let attempt_status = payment_data.payment_attempt.status.to_owned();
let connector_status = router_data.status.to_owned();
let updated_attempt_status = match (
connector_status,
attempt_status,
payment_data.frm_message.to_owned(),
) {
(
enums::AttemptStatus::Authorized,
enums::AttemptStatus::Unresolved,
Some(frm_message),
) => match frm_message.frm_status {
enums::FraudCheckStatus::Fraud | enums::FraudCheckStatus::ManualReview => {
attempt_status
}
_ => router_data.get_attempt_status_for_db_update(&payment_data),
},
_ => router_data.get_attempt_status_for_db_update(&payment_data),
};
match payments_response {
types::PaymentsResponseData::PreProcessingResponse {
pre_processing_id,
connector_metadata,
connector_response_reference_id,
..
} => {
let connector_transaction_id = match pre_processing_id.to_owned() {
types::PreprocessingResponseId::PreProcessingId(_) => None,
types::PreprocessingResponseId::ConnectorTransactionId(
connector_txn_id,
) => Some(connector_txn_id),
};
let preprocessing_step_id = match pre_processing_id {
types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => {
Some(pre_processing_id)
}
types::PreprocessingResponseId::ConnectorTransactionId(_) => None,
};
let payment_attempt_update =
storage::PaymentAttemptUpdate::PreprocessingUpdate {
status: updated_attempt_status,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
connector_metadata,
preprocessing_step_id,
connector_transaction_id,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
};
(None, Some(payment_attempt_update))
}
types::PaymentsResponseData::TransactionResponse {
resource_id,
redirection_data,
connector_metadata,
connector_response_reference_id,
incremental_authorization_allowed,
charge_id,
..
} => {
payment_data
.payment_intent
.incremental_authorization_allowed =
core_utils::get_incremental_authorization_allowed_value(
incremental_authorization_allowed,
payment_data
.payment_intent
.request_incremental_authorization,
);
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
let encoded_data = payment_data.payment_attempt.encoded_data.clone();
let authentication_data = redirection_data
.as_ref()
.map(Encode::encode_to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not parse the connector response")?;
// match on connector integrity check
match router_data.integrity_check.clone() {
Err(err) => {
let auth_update = if Some(router_data.auth_type)
!= payment_data.payment_attempt.authentication_type
{
@ -963,133 +880,259 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
} else {
None
};
// incase of success, update error code and error message
let error_status = if router_data.status == enums::AttemptStatus::Charged {
Some(None)
} else {
None
};
if router_data.status == enums::AttemptStatus::Charged {
payment_data
.payment_intent
.fingerprint_id
.clone_from(&payment_data.payment_attempt.fingerprint_id);
metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]);
}
let payment_method_id = payment_data.payment_attempt.payment_method_id.clone();
utils::add_apple_pay_payment_status_metrics(
router_data.status,
router_data.apple_pay_flow.clone(),
payment_data.payment_attempt.connector.clone(),
payment_data.payment_attempt.merchant_id.clone(),
);
let (capture_updates, payment_attempt_update) = match payment_data
.multiple_capture_data
{
Some(multiple_capture_data) => {
let capture_update = storage::CaptureUpdate::ResponseUpdate {
status: enums::CaptureStatus::foreign_try_from(router_data.status)?,
connector_capture_id: connector_transaction_id.clone(),
connector_response_reference_id,
};
let capture_update_list = vec![(
multiple_capture_data.get_latest_capture().clone(),
capture_update,
)];
(
Some((multiple_capture_data, capture_update_list)),
auth_update.map(|auth_type| {
storage::PaymentAttemptUpdate::AuthenticationTypeUpdate {
authentication_type: auth_type,
updated_by: storage_scheme.to_string(),
}
}),
)
}
None => (
None,
Some(storage::PaymentAttemptUpdate::ResponseUpdate {
status: updated_attempt_status,
connector: None,
connector_transaction_id: connector_transaction_id.clone(),
authentication_type: auth_update,
amount_capturable: router_data
.request
.get_amount_capturable(&payment_data, updated_attempt_status)
.map(MinorUnit::new),
payment_method_id,
mandate_id: payment_data.payment_attempt.mandate_id.clone(),
connector_metadata,
payment_token: None,
error_code: error_status.clone(),
error_message: error_status.clone(),
error_reason: error_status.clone(),
unified_code: error_status.clone(),
unified_message: error_status,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
authentication_data,
encoded_data,
payment_method_data: additional_payment_method_data,
charge_id,
}),
),
};
(capture_updates, payment_attempt_update)
}
types::PaymentsResponseData::TransactionUnresolvedResponse {
resource_id,
reason,
connector_response_reference_id,
} => {
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
let field_name = err.field_names;
let connector_transaction_id = err.connector_transaction_id;
(
None,
Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate {
status: updated_attempt_status,
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
connector: None,
connector_transaction_id,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
error_code: Some(reason.clone().map(|cd| cd.code)),
error_message: Some(reason.clone().map(|cd| cd.message)),
error_reason: Some(reason.map(|cd| cd.message)),
connector_response_reference_id,
status: enums::AttemptStatus::Pending,
error_message: Some(Some("Integrity Check Failed!".to_string())),
error_code: Some(Some("IE".to_string())),
error_reason: Some(Some(format!(
"Integrity Check Failed! Value mismatched for fields {field_name}"
))),
amount_capturable: None,
updated_by: storage_scheme.to_string(),
unified_code: None,
unified_message: None,
connector_transaction_id,
payment_method_data: None,
authentication_type: auth_update,
}),
)
}
types::PaymentsResponseData::SessionResponse { .. } => (None, None),
types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None),
types::PaymentsResponseData::TokenizationResponse { .. } => (None, None),
types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None),
types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None),
types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => {
(None, None)
}
types::PaymentsResponseData::MultipleCaptureResponse {
capture_sync_response_list,
} => match payment_data.multiple_capture_data {
Some(multiple_capture_data) => {
let capture_update_list = response_to_capture_update(
&multiple_capture_data,
Ok(()) => {
let attempt_status = payment_data.payment_attempt.status.to_owned();
let connector_status = router_data.status.to_owned();
let updated_attempt_status = match (
connector_status,
attempt_status,
payment_data.frm_message.to_owned(),
) {
(
enums::AttemptStatus::Authorized,
enums::AttemptStatus::Unresolved,
Some(frm_message),
) => match frm_message.frm_status {
enums::FraudCheckStatus::Fraud
| enums::FraudCheckStatus::ManualReview => attempt_status,
_ => router_data.get_attempt_status_for_db_update(&payment_data),
},
_ => router_data.get_attempt_status_for_db_update(&payment_data),
};
match payments_response {
types::PaymentsResponseData::PreProcessingResponse {
pre_processing_id,
connector_metadata,
connector_response_reference_id,
..
} => {
let connector_transaction_id = match pre_processing_id.to_owned() {
types::PreprocessingResponseId::PreProcessingId(_) => None,
types::PreprocessingResponseId::ConnectorTransactionId(
connector_txn_id,
) => Some(connector_txn_id),
};
let preprocessing_step_id = match pre_processing_id {
types::PreprocessingResponseId::PreProcessingId(
pre_processing_id,
) => Some(pre_processing_id),
types::PreprocessingResponseId::ConnectorTransactionId(_) => None,
};
let payment_attempt_update =
storage::PaymentAttemptUpdate::PreprocessingUpdate {
status: updated_attempt_status,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
connector_metadata,
preprocessing_step_id,
connector_transaction_id,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
};
(None, Some(payment_attempt_update))
}
types::PaymentsResponseData::TransactionResponse {
resource_id,
redirection_data,
connector_metadata,
connector_response_reference_id,
incremental_authorization_allowed,
charge_id,
..
} => {
payment_data
.payment_intent
.incremental_authorization_allowed =
core_utils::get_incremental_authorization_allowed_value(
incremental_authorization_allowed,
payment_data
.payment_intent
.request_incremental_authorization,
);
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
let encoded_data = payment_data.payment_attempt.encoded_data.clone();
let authentication_data = redirection_data
.as_ref()
.map(Encode::encode_to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not parse the connector response")?;
let auth_update = if Some(router_data.auth_type)
!= payment_data.payment_attempt.authentication_type
{
Some(router_data.auth_type)
} else {
None
};
// incase of success, update error code and error message
let error_status =
if router_data.status == enums::AttemptStatus::Charged {
Some(None)
} else {
None
};
if router_data.status == enums::AttemptStatus::Charged {
payment_data
.payment_intent
.fingerprint_id
.clone_from(&payment_data.payment_attempt.fingerprint_id);
metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]);
}
let payment_method_id =
payment_data.payment_attempt.payment_method_id.clone();
utils::add_apple_pay_payment_status_metrics(
router_data.status,
router_data.apple_pay_flow.clone(),
payment_data.payment_attempt.connector.clone(),
payment_data.payment_attempt.merchant_id.clone(),
);
let (capture_updates, payment_attempt_update) = match payment_data
.multiple_capture_data
{
Some(multiple_capture_data) => {
let capture_update = storage::CaptureUpdate::ResponseUpdate {
status: enums::CaptureStatus::foreign_try_from(
router_data.status,
)?,
connector_capture_id: connector_transaction_id.clone(),
connector_response_reference_id,
};
let capture_update_list = vec![(
multiple_capture_data.get_latest_capture().clone(),
capture_update,
)];
(Some((multiple_capture_data, capture_update_list)), auth_update.map(|auth_type| {
storage::PaymentAttemptUpdate::AuthenticationTypeUpdate {
authentication_type: auth_type,
updated_by: storage_scheme.to_string(),
}
}))
}
None => (
None,
Some(storage::PaymentAttemptUpdate::ResponseUpdate {
status: updated_attempt_status,
connector: None,
connector_transaction_id: connector_transaction_id.clone(),
authentication_type: auth_update,
amount_capturable: router_data
.request
.get_amount_capturable(
&payment_data,
updated_attempt_status,
)
.map(MinorUnit::new),
payment_method_id,
mandate_id: payment_data.payment_attempt.mandate_id.clone(),
connector_metadata,
payment_token: None,
error_code: error_status.clone(),
error_message: error_status.clone(),
error_reason: error_status.clone(),
unified_code: error_status.clone(),
unified_message: error_status,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
authentication_data,
encoded_data,
payment_method_data: additional_payment_method_data,
charge_id,
}),
),
};
(capture_updates, payment_attempt_update)
}
types::PaymentsResponseData::TransactionUnresolvedResponse {
resource_id,
reason,
connector_response_reference_id,
} => {
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
(
None,
Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate {
status: updated_attempt_status,
connector: None,
connector_transaction_id,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
error_code: Some(reason.clone().map(|cd| cd.code)),
error_message: Some(reason.clone().map(|cd| cd.message)),
error_reason: Some(reason.map(|cd| cd.message)),
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
}),
)
}
types::PaymentsResponseData::SessionResponse { .. } => (None, None),
types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None),
types::PaymentsResponseData::TokenizationResponse { .. } => (None, None),
types::PaymentsResponseData::ConnectorCustomerResponse { .. } => {
(None, None)
}
types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => {
(None, None)
}
types::PaymentsResponseData::IncrementalAuthorizationResponse {
..
} => (None, None),
types::PaymentsResponseData::MultipleCaptureResponse {
capture_sync_response_list,
)?;
(Some((multiple_capture_data, capture_update_list)), None)
} => match payment_data.multiple_capture_data {
Some(multiple_capture_data) => {
let capture_update_list = response_to_capture_update(
&multiple_capture_data,
capture_sync_response_list,
)?;
(Some((multiple_capture_data, capture_update_list)), None)
}
None => (None, None),
},
}
None => (None, None),
},
}
}
}
};
@ -1308,7 +1351,33 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
.as_mut()
.map(|info| info.status = status)
});
Ok(payment_data)
match router_data.integrity_check {
Ok(()) => Ok(payment_data),
Err(err) => {
metrics::INTEGRITY_CHECK_FAILED.add(
&metrics::CONTEXT,
1,
&add_attributes([
(
"connector",
payment_data.payment_attempt.connector.unwrap_or_default(),
),
("merchant_id", payment_data.payment_attempt.merchant_id),
]),
);
Err(error_stack::Report::new(
errors::ApiErrorResponse::IntegrityCheckFailed {
reason: payment_data
.payment_attempt
.error_message
.unwrap_or_default(),
field_names: err.field_names,
connector_transaction_id: payment_data.payment_attempt.connector_transaction_id,
},
))
}
}
}
async fn update_payment_method_status_and_ntid<F: Clone>(

View File

@ -195,6 +195,7 @@ where
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
@ -1349,6 +1350,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
.transpose()?,
customer_acceptance: payment_data.customer_acceptance,
charges,
integrity_object: None,
})
}
}
@ -1358,7 +1360,14 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data;
let amount = payment_data
.surcharge_details
.as_ref()
.map(|surcharge_details| surcharge_details.final_amount)
.unwrap_or(payment_data.amount.into());
Ok(Self {
amount,
integrity_object: None,
mandate_id: payment_data.mandate_id.clone(),
connector_transaction_id: match payment_data.payment_attempt.connector_transaction_id {
Some(connector_txn_id) => {

View File

@ -209,6 +209,7 @@ pub async fn construct_payout_router_data<'a, F>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
@ -371,6 +372,7 @@ pub async fn construct_refund_router_data<'a, F>(
refund_id: Some(refund.refund_id.clone()),
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
@ -608,6 +610,7 @@ pub async fn construct_accept_dispute_router_data<'a>(
dispute_id: Some(dispute.dispute_id.clone()),
refund_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -703,6 +706,7 @@ pub async fn construct_submit_evidence_router_data<'a>(
refund_id: None,
dispute_id: Some(dispute.dispute_id.clone()),
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -804,6 +808,7 @@ pub async fn construct_upload_file_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -902,6 +907,7 @@ pub async fn construct_defend_dispute_router_data<'a>(
refund_id: None,
dispute_id: Some(dispute.dispute_id.clone()),
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -989,6 +995,7 @@ pub async fn construct_retrieve_file_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}

View File

@ -119,6 +119,7 @@ pub async fn construct_webhook_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}