mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
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:
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -112,6 +112,7 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::PaymentMethodsMigrate
|
||||
| Flow::PaymentMethodsList
|
||||
| Flow::CustomerPaymentMethodsList
|
||||
| Flow::GetPaymentMethodTokenData
|
||||
| Flow::PaymentMethodsRetrieve
|
||||
| Flow::PaymentMethodsUpdate
|
||||
| Flow::PaymentMethodsDelete
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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(
|
||||
|
||||
Reference in New Issue
Block a user