feat(payment_methods): add v2 api for fetching token data (#7629)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sakil Mostak
2025-05-12 19:15:53 +05:30
committed by GitHub
parent 9da96e890c
commit 2cefac5cb3
16 changed files with 486 additions and 16 deletions

View File

@ -50,11 +50,13 @@ use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData};
feature = "customer_v2"
))]
use hyperswitch_domain_models::mandates::CommonMandateReference;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use hyperswitch_domain_models::payment_method_data;
use hyperswitch_domain_models::payments::{
payment_attempt::PaymentAttempt, PaymentIntent, VaultData,
};
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use hyperswitch_domain_models::{payment_method_data, payment_methods as domain_payment_methods};
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use masking::ExposeOptionInterface;
use masking::{PeekInterface, Secret};
use router_env::{instrument, tracing};
use time::Duration;
@ -1363,6 +1365,102 @@ pub async fn list_saved_payment_methods_for_customer(
))
}
#[cfg(all(feature = "v2", feature = "olap"))]
#[instrument(skip_all)]
pub async fn get_token_data_for_payment_method(
state: SessionState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
profile: domain::Profile,
request: payment_methods::GetTokenDataRequest,
payment_method_id: id_type::GlobalPaymentMethodId,
) -> RouterResponse<api::TokenDataResponse> {
let key_manager_state = &(&state).into();
let db = &*state.store;
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::PaymentMethodNotFound)?;
let token_data_response =
generate_token_data_response(&state, request, profile, &payment_method).await?;
Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
token_data_response,
))
}
#[cfg(all(feature = "v2", feature = "olap"))]
#[instrument(skip_all)]
pub async fn generate_token_data_response(
state: &SessionState,
request: payment_methods::GetTokenDataRequest,
profile: domain::Profile,
payment_method: &domain_payment_methods::PaymentMethod,
) -> RouterResult<api::TokenDataResponse> {
let token_details = match request.token_type {
common_enums::TokenDataType::NetworkToken => {
let is_network_tokenization_enabled = profile.is_network_tokenization_enabled;
if !is_network_tokenization_enabled {
return Err(errors::ApiErrorResponse::UnprocessableEntity {
message: "Network tokenization is not enabled for this profile".to_string(),
}
.into());
}
let network_token_requestor_ref_id = payment_method
.network_token_requestor_reference_id
.clone()
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
message: "NetworkTokenRequestorReferenceId is not present".to_string(),
})?;
let network_token = network_tokenization::get_token_from_tokenization_service(
state,
network_token_requestor_ref_id,
payment_method,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to fetch network token data from tokenization service")?;
api::TokenDetailsResponse::NetworkTokenDetails(api::NetworkTokenDetailsResponse {
network_token: network_token.network_token,
network_token_exp_month: network_token.network_token_exp_month,
network_token_exp_year: network_token.network_token_exp_year,
cryptogram: network_token.cryptogram,
card_issuer: network_token.card_issuer,
card_network: network_token.card_network,
card_type: network_token.card_type,
card_issuing_country: network_token.card_issuing_country,
bank_code: network_token.bank_code,
card_holder_name: network_token.card_holder_name,
nick_name: network_token.nick_name,
eci: network_token.eci,
})
}
common_enums::TokenDataType::SingleUseToken
| common_enums::TokenDataType::MultiUseToken => {
return Err(errors::ApiErrorResponse::UnprocessableEntity {
message: "Token type not supported".to_string(),
}
.into());
}
};
Ok(api::TokenDataResponse {
payment_method_id: payment_method.id.clone(),
token_type: request.token_type,
token_details,
})
}
#[cfg(all(feature = "v2", feature = "olap"))]
#[instrument(skip_all)]
pub async fn get_total_saved_payment_methods_for_merchant(

View File

@ -1,5 +1,7 @@
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use std::fmt::Debug;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use std::str::FromStr;
use api_models::payment_methods as api_payment_methods;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
@ -19,7 +21,9 @@ use error_stack::ResultExt;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use error_stack::{report, ResultExt};
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use hyperswitch_domain_models::payment_method_data::NetworkTokenDetails;
use hyperswitch_domain_models::payment_method_data::{
NetworkTokenDetails, NetworkTokenDetailsPaymentMethod,
};
use josekit::jwe;
use masking::{ExposeInterface, Mask, PeekInterface, Secret};
@ -575,6 +579,82 @@ pub async fn get_token_from_tokenization_service(
Ok(network_token_data)
}
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub async fn get_token_from_tokenization_service(
state: &routes::SessionState,
network_token_requestor_ref_id: String,
pm_data: &domain::PaymentMethod,
) -> errors::RouterResult<domain::NetworkTokenData> {
let token_response =
if let Some(network_tokenization_service) = &state.conf.network_tokenization_service {
record_operation_time(
async {
get_network_token(
state,
&pm_data.customer_id,
network_token_requestor_ref_id,
network_tokenization_service.get_inner(),
)
.await
.inspect_err(
|e| logger::error!(error=?e, "Error while fetching token from tokenization service")
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Fetch network token failed")
},
&metrics::FETCH_NETWORK_TOKEN_TIME,
&[],
)
.await
} else {
Err(errors::NetworkTokenizationError::NetworkTokenizationServiceNotConfigured)
.inspect_err(|err| {
logger::error!(error=? err);
})
.change_context(errors::ApiErrorResponse::InternalServerError)
}?;
let token_decrypted = pm_data
.network_token_payment_method_data
.clone()
.map(|value| value.into_inner())
.and_then(|payment_method_data| match payment_method_data {
hyperswitch_domain_models::payment_method_data::PaymentMethodsData::NetworkToken(
token,
) => Some(token),
_ => None,
})
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to obtain decrypted token object from db")?;
let network_token_data = domain::NetworkTokenData {
network_token: token_response.authentication_details.token,
cryptogram: Some(token_response.authentication_details.cryptogram),
network_token_exp_month: token_decrypted
.network_token_expiry_month
.unwrap_or(token_response.token_details.exp_month),
network_token_exp_year: token_decrypted
.network_token_expiry_year
.unwrap_or(token_response.token_details.exp_year),
card_holder_name: token_decrypted.card_holder_name,
nick_name: token_decrypted.nick_name.or(token_response.nickname),
card_issuer: token_decrypted.card_issuer.or(token_response.issuer),
card_network: Some(token_response.network),
card_type: token_decrypted
.card_type
.or(token_response.card_type)
.as_ref()
.map(|c| api_payment_methods::CardType::from_str(c))
.transpose()
.ok()
.flatten(),
card_issuing_country: token_decrypted.issuer_country,
bank_code: None,
eci: token_response.eci,
};
Ok(network_token_data)
}
#[cfg(feature = "v1")]
pub async fn do_status_check_for_network_token(
state: &routes::SessionState,

View File

@ -1252,6 +1252,10 @@ impl PaymentMethods {
.service(
web::resource("/update-saved-payment-method")
.route(web::put().to(payment_methods::payment_method_update_api)),
)
.service(
web::resource("/get-token")
.route(web::get().to(payment_methods::get_payment_method_token_data)),
),
);

View File

@ -112,6 +112,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::PaymentMethodsMigrate
| Flow::PaymentMethodsList
| Flow::CustomerPaymentMethodsList
| Flow::GetPaymentMethodTokenData
| Flow::PaymentMethodsRetrieve
| Flow::PaymentMethodsUpdate
| Flow::PaymentMethodsDelete

View File

@ -676,6 +676,48 @@ pub async fn list_customer_payment_method_api(
.await
}
#[cfg(all(feature = "v2", feature = "olap"))]
#[instrument(skip_all, fields(flow = ?Flow::GetPaymentMethodTokenData))]
pub async fn get_payment_method_token_data(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<id_type::GlobalPaymentMethodId>,
json_payload: web::Json<api_models::payment_methods::GetTokenDataRequest>,
) -> HttpResponse {
let flow = Flow::GetPaymentMethodTokenData;
let payment_method_id = path.into_inner();
let payload = json_payload.into_inner();
Box::pin(api::server_wrap(
flow,
state,
&req,
payload,
|state, auth: auth::AuthenticationData, req, _| {
payment_methods_routes::get_token_data_for_payment_method(
state,
auth.merchant_account,
auth.key_store,
auth.profile,
req,
payment_method_id.clone(),
)
},
auth::auth_type(
&auth::V2ApiKeyAuth {
is_connected_allowed: false,
is_platform_allowed: false,
},
&auth::JWTAuth {
permission: Permission::MerchantCustomerRead,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(all(feature = "v2", feature = "olap"))]
#[instrument(skip_all, fields(flow = ?Flow::TotalPaymentMethodCount))]
pub async fn get_total_payment_method_count(

View File

@ -4,13 +4,14 @@ pub use api_models::payment_methods::{
CardNetworkTokenizeResponse, CardType, CustomerPaymentMethod,
CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest,
GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, MigrateCardDetail,
NetworkTokenDetailsPaymentMethod, NetworkTokenResponse, PaymentMethodCollectLinkRenderRequest,
PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData,
PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm,
PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListRequest,
PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodMigrateResponse,
PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData,
PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1,
NetworkTokenDetailsPaymentMethod, NetworkTokenDetailsResponse, NetworkTokenResponse,
PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate,
PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId,
PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodListData,
PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate,
PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodResponseData,
PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, TokenDataResponse,
TokenDetailsResponse, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1,
TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2,
TotalPaymentMethodCountResponse,
};

View File

@ -270,12 +270,21 @@ pub struct GetCardToken {
pub card_reference: String,
pub customer_id: id_type::GlobalCustomerId,
}
#[cfg(feature = "v1")]
#[derive(Debug, Deserialize)]
pub struct AuthenticationDetails {
pub cryptogram: Secret<String>,
pub token: CardNumber, //network token
}
#[cfg(feature = "v2")]
#[derive(Debug, Deserialize)]
pub struct AuthenticationDetails {
pub cryptogram: Secret<String>,
pub token: NetworkToken, //network token
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenDetails {
pub exp_month: Secret<String>,
@ -287,6 +296,10 @@ pub struct TokenResponse {
pub authentication_details: AuthenticationDetails,
pub network: api_enums::CardNetwork,
pub token_details: TokenDetails,
pub eci: Option<String>,
pub card_type: Option<String>,
pub issuer: Option<String>,
pub nickname: Option<Secret<String>>,
}
#[cfg(all(