feat(core): [NETWORK TOKENIZATION] Check Network Token Status API (#9443)

This commit is contained in:
Sagnik Mitra
2025-10-10 17:27:47 +05:30
committed by GitHub
parent 115ef10aef
commit d9d4b2e5e4
13 changed files with 525 additions and 20 deletions

View File

@ -284,6 +284,12 @@
"v2/payment-methods/list-saved-payment-methods-for-a-customer"
]
},
{
"group": "Network Tokenization",
"pages": [
"v2/payment-methods/check-network-token-status"
]
},
{
"group": "Payment Method Session",
"pages": [

View File

@ -2891,6 +2891,47 @@
]
}
},
"/v2/payment-methods/{payment_method_id}/check-network-token-status": {
"get": {
"tags": [
"Payment Methods"
],
"summary": "Payment Method - Check Network Token Status",
"description": "Check the status of a network token for a saved payment method",
"operationId": "Check Network Token Status",
"parameters": [
{
"name": "payment_method_id",
"in": "path",
"description": "The unique identifier for the Payment Method",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Network Token Status Retrieved",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NetworkTokenStatusCheckResponse"
}
}
}
},
"404": {
"description": "Payment Method Not Found"
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v2/customers/{id}/saved-payment-methods": {
"get": {
"tags": [
@ -15305,6 +15346,115 @@
}
}
},
"NetworkTokenStatusCheckFailureResponse": {
"type": "object",
"required": [
"error_message"
],
"properties": {
"error_message": {
"type": "string",
"description": "Error message describing what went wrong"
}
}
},
"NetworkTokenStatusCheckResponse": {
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/NetworkTokenStatusCheckSuccessResponse"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"success_response"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/NetworkTokenStatusCheckFailureResponse"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"failure_response"
]
}
}
}
]
}
],
"discriminator": {
"propertyName": "type"
}
},
"NetworkTokenStatusCheckSuccessResponse": {
"type": "object",
"required": [
"status",
"token_expiry_month",
"token_expiry_year",
"card_last_four",
"token_last_four",
"card_expiry",
"payment_method_id",
"customer_id"
],
"properties": {
"status": {
"$ref": "#/components/schemas/TokenStatus"
},
"token_expiry_month": {
"type": "string",
"description": "The expiry month of the network token if active"
},
"token_expiry_year": {
"type": "string",
"description": "The expiry year of the network token if active"
},
"card_last_four": {
"type": "string",
"description": "The last four digits of the card"
},
"token_last_four": {
"type": "string",
"description": "The last four digits of the network token"
},
"card_expiry": {
"type": "string",
"description": "The expiry date of the card in MM/YY format"
},
"payment_method_id": {
"type": "string",
"description": "The payment method ID that was checked",
"example": "12345_pm_019959146f92737389eb6927ce1eb7dc"
},
"customer_id": {
"type": "string",
"description": "The customer ID associated with the payment method",
"example": "12345_cus_0195dc62bb8e7312a44484536da76aef"
}
}
},
"NetworkTokenization": {
"type": "object",
"description": "The network tokenization configuration for creating the payment method session",
@ -26042,6 +26192,14 @@
}
]
},
"TokenStatus": {
"type": "string",
"enum": [
"ACTIVE",
"SUSPENDED",
"DEACTIVATED"
]
},
"TokenType": {
"type": "string",
"enum": [

View File

@ -492,3 +492,29 @@ impl From<PermissionScope> for ReconPermissionScope {
}
}
}
#[cfg(feature = "v2")]
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
PartialEq,
ToSchema,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumIter,
strum::EnumString,
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum TokenStatus {
/// Indicates that the token is active and can be used for payments
Active,
/// Indicates that the token is suspended from network's end for some reason and can't be used for payments until it is re-activated
Suspended,
/// Indicates that the token is deactivated and further can't be used for payments
Deactivated,
}

View File

@ -3262,3 +3262,56 @@ pub struct AuthenticationDetails {
#[schema(value_type = Option<ErrorDetails>)]
pub error: Option<payments::ErrorDetails>,
}
#[cfg(feature = "v2")]
#[derive(Debug, serde::Serialize, ToSchema)]
pub struct NetworkTokenStatusCheckSuccessResponse {
/// The status of the network token
#[schema(value_type = TokenStatus)]
pub status: api_enums::TokenStatus,
/// The expiry month of the network token if active
#[schema(value_type = String)]
pub token_expiry_month: masking::Secret<String>,
/// The expiry year of the network token if active
#[schema(value_type = String)]
pub token_expiry_year: masking::Secret<String>,
/// The last four digits of the card
pub card_last_four: String,
/// The last four digits of the network token
pub token_last_four: String,
/// The expiry date of the card in MM/YY format
pub card_expiry: String,
/// The payment method ID that was checked
#[schema(value_type = String, example = "12345_pm_019959146f92737389eb6927ce1eb7dc")]
pub payment_method_id: id_type::GlobalPaymentMethodId,
/// The customer ID associated with the payment method
#[schema(value_type = String, example = "12345_cus_0195dc62bb8e7312a44484536da76aef")]
pub customer_id: id_type::GlobalCustomerId,
}
#[cfg(feature = "v2")]
impl common_utils::events::ApiEventMetric for NetworkTokenStatusCheckResponse {}
#[cfg(feature = "v2")]
#[derive(Debug, serde::Serialize, ToSchema)]
pub struct NetworkTokenStatusCheckFailureResponse {
/// Error message describing what went wrong
pub error_message: String,
}
#[cfg(feature = "v2")]
#[derive(Debug, serde::Serialize, ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum NetworkTokenStatusCheckResponse {
/// Successful network token status check response
SuccessResponse(NetworkTokenStatusCheckSuccessResponse),
/// Error response for network token status check
FailureResponse(NetworkTokenStatusCheckFailureResponse),
}

View File

@ -137,6 +137,7 @@ Never share your secret api keys. Keep them guarded and secure.
routes::payment_method::payment_method_update_api,
routes::payment_method::payment_method_retrieve_api,
routes::payment_method::payment_method_delete_api,
routes::payment_method::network_token_status_check_api,
routes::payment_method::list_customer_payment_method_api,
//Routes for payment method session
@ -273,6 +274,10 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payment_methods::RequestPaymentMethodTypes,
api_models::payment_methods::CardType,
api_models::payment_methods::PaymentMethodListData,
api_models::payment_methods::NetworkTokenStatusCheckResponse,
api_models::payment_methods::NetworkTokenStatusCheckSuccessResponse,
api_models::payment_methods::NetworkTokenStatusCheckFailureResponse,
api_models::enums::TokenStatus,
api_models::poll::PollResponse,
api_models::poll::PollStatus,
api_models::customers::CustomerResponse,

View File

@ -325,6 +325,26 @@ pub async fn payment_method_update_api() {}
#[cfg(feature = "v2")]
pub async fn payment_method_delete_api() {}
/// Payment Method - Check Network Token Status
///
/// Check the status of a network token for a saved payment method
#[utoipa::path(
get,
path = "/v2/payment-methods/{payment_method_id}/check-network-token-status",
params (
("payment_method_id" = String, Path, description = "The unique identifier for the Payment Method"),
),
responses(
(status = 200, description = "Network Token Status Retrieved", body = NetworkTokenStatusCheckResponse),
(status = 404, description = "Payment Method Not Found"),
),
tag = "Payment Methods",
operation_id = "Check Network Token Status",
security(("api_key" = []))
)]
#[cfg(feature = "v2")]
pub async fn network_token_status_check_api() {}
/// Payment Method - List Customer Saved Payment Methods
///
/// List the payment methods saved for a customer

View File

@ -3913,3 +3913,99 @@ async fn get_single_use_token_from_store(
.change_context(errors::StorageError::KVError)
.attach_printable("Failed to get payment method token from redis")
}
#[cfg(feature = "v2")]
#[instrument(skip_all)]
async fn fetch_payment_method(
state: &SessionState,
merchant_context: &domain::MerchantContext,
payment_method_id: &id_type::GlobalPaymentMethodId,
) -> RouterResult<domain::PaymentMethod> {
let db = &state.store;
let key_manager_state = &state.into();
let merchant_account = merchant_context.get_merchant_account();
let key_store = merchant_context.get_merchant_key_store();
db.find_payment_method(
key_manager_state,
key_store,
payment_method_id,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)
.attach_printable("Payment method not found for network token status check")
}
#[cfg(feature = "v2")]
pub async fn check_network_token_status(
state: SessionState,
merchant_context: domain::MerchantContext,
payment_method_id: id_type::GlobalPaymentMethodId,
) -> RouterResponse<payment_methods::NetworkTokenStatusCheckResponse> {
// Retrieve the payment method from the database
let payment_method =
fetch_payment_method(&state, &merchant_context, &payment_method_id).await?;
// Call the network token status check function
let network_token_status_check_response = if payment_method.status
== common_enums::PaymentMethodStatus::Active
{
// Check if the payment method has network token data
when(
payment_method
.network_token_requestor_reference_id
.is_none(),
|| {
Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "payment_method_id",
})
},
)?;
match network_tokenization::do_status_check_for_network_token(&state, &payment_method).await
{
Ok(network_token_details) => {
let status = match network_token_details.token_status {
pm_types::TokenStatus::Active => api_enums::TokenStatus::Active,
pm_types::TokenStatus::Suspended => api_enums::TokenStatus::Suspended,
pm_types::TokenStatus::Deactivated => api_enums::TokenStatus::Deactivated,
};
payment_methods::NetworkTokenStatusCheckResponse::SuccessResponse(
payment_methods::NetworkTokenStatusCheckSuccessResponse {
status,
token_expiry_month: network_token_details.token_expiry_month,
token_expiry_year: network_token_details.token_expiry_year,
card_last_four: network_token_details.card_last_4,
card_expiry: network_token_details.card_expiry,
token_last_four: network_token_details.token_last_4,
payment_method_id,
customer_id: payment_method.customer_id,
},
)
}
Err(e) => {
let err_message = e.current_context().to_string();
logger::debug!("Network token status check failed: {:?}", e);
payment_methods::NetworkTokenStatusCheckResponse::FailureResponse(
payment_methods::NetworkTokenStatusCheckFailureResponse {
error_message: err_message,
},
)
}
}
} else {
let err_message = "Payment Method is not active".to_string();
logger::debug!("Payment Method is not active");
payment_methods::NetworkTokenStatusCheckResponse::FailureResponse(
payment_methods::NetworkTokenStatusCheckFailureResponse {
error_message: err_message,
},
)
};
Ok(services::ApplicationResponse::Json(
network_token_status_check_response,
))
}

View File

@ -778,24 +778,125 @@ pub async fn check_token_status_with_tokenization_service(
.parse_struct("Delete Network Tokenization Response")
.change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?;
match check_token_status_response.payload.token_status {
match check_token_status_response.token_status {
pm_types::TokenStatus::Active => Ok((
Some(check_token_status_response.payload.token_expiry_month),
Some(check_token_status_response.payload.token_expiry_year),
Some(check_token_status_response.token_expiry_month),
Some(check_token_status_response.token_expiry_year),
)),
pm_types::TokenStatus::Inactive => Ok((None, None)),
_ => Ok((None, None)),
}
}
#[cfg(feature = "v2")]
pub async fn check_token_status_with_tokenization_service(
_state: &routes::SessionState,
_customer_id: &id_type::GlobalCustomerId,
_network_token_requestor_reference_id: String,
_tokenization_service: &settings::NetworkTokenizationService,
) -> CustomResult<(Option<Secret<String>>, Option<Secret<String>>), errors::NetworkTokenizationError>
{
todo!()
state: &routes::SessionState,
customer_id: &id_type::GlobalCustomerId,
network_token_requestor_reference_id: String,
tokenization_service: &settings::NetworkTokenizationService,
) -> CustomResult<pm_types::CheckTokenStatusResponse, errors::NetworkTokenizationError> {
let mut request = services::Request::new(
services::Method::Post,
tokenization_service.check_token_status_url.as_str(),
);
let payload = pm_types::CheckTokenStatus {
card_reference: network_token_requestor_reference_id,
customer_id: customer_id.clone(),
};
request.add_header(headers::CONTENT_TYPE, "application/json".into());
request.add_header(
headers::AUTHORIZATION,
tokenization_service
.token_service_api_key
.clone()
.peek()
.clone()
.into_masked(),
);
request.add_default_headers();
request.set_body(RequestContent::Json(Box::new(payload)));
// Send the request using `call_connector_api`
let response = services::call_connector_api(state, request, "Check Network token Status")
.await
.change_context(errors::NetworkTokenizationError::ApiError);
let res = response
.change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)
.attach_printable("Error while receiving response")
.and_then(|inner| match inner {
Err(err_res) => {
let parsed_error: pm_types::NetworkTokenErrorResponse = err_res
.response
.parse_struct("Network Tokenization Error Response")
.change_context(
errors::NetworkTokenizationError::ResponseDeserializationFailed,
)?;
logger::error!(
error_code = %parsed_error.error_info.code,
developer_message = %parsed_error.error_info.developer_message,
"Network tokenization error: {}",
parsed_error.error_message
);
Err(errors::NetworkTokenizationError::ResponseDeserializationFailed)
.attach_printable(format!("Response Deserialization Failed: {err_res:?}"))
}
Ok(res) => Ok(res),
})
.inspect_err(|err| {
logger::error!("Error while deserializing response: {:?}", err);
})?;
let check_token_status_response: pm_types::CheckTokenStatusResponse = res
.response
.parse_struct("CheckTokenStatusResponse")
.change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?;
Ok(check_token_status_response)
}
#[cfg(feature = "v2")]
pub async fn do_status_check_for_network_token(
state: &routes::SessionState,
payment_method_info: &domain::PaymentMethod,
) -> CustomResult<pm_types::CheckTokenStatusResponse, errors::NetworkTokenizationError> {
let network_token_requestor_reference_id = payment_method_info
.network_token_requestor_reference_id
.clone();
if let Some(ref_id) = network_token_requestor_reference_id {
if let Some(network_tokenization_service) = &state.conf.network_tokenization_service {
let network_token_details = record_operation_time(
async {
check_token_status_with_tokenization_service(
state,
&payment_method_info.customer_id,
ref_id,
network_tokenization_service.get_inner(),
)
.await
.inspect_err(
|e| logger::error!(error=?e, "Error while fetching token from tokenization service")
)
.attach_printable(
"Check network token status with tokenization service failed",
)
},
&metrics::CHECK_NETWORK_TOKEN_STATUS_TIME,
&[],
)
.await?;
Ok(network_token_details)
} else {
Err(errors::NetworkTokenizationError::NetworkTokenizationServiceNotConfigured)
.attach_printable("Network Tokenization Service not configured")
.inspect_err(|_| {
logger::error!("Network Tokenization Service not configured");
})
}
} else {
Err(errors::NetworkTokenizationError::FetchNetworkTokenFailed)
.attach_printable("Check network token status failed")?
}
}
#[cfg(feature = "v1")]

View File

@ -1455,6 +1455,10 @@ impl PaymentMethods {
.service(
web::resource("/create-intent")
.route(web::post().to(payment_methods::create_payment_method_intent_api)),
)
.service(
web::resource("/{payment_method_id}/check-network-token-status")
.route(web::get().to(payment_methods::network_token_status_check_api)),
);
route = route.service(

View File

@ -126,6 +126,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::PaymentMethodsRetrieve
| Flow::PaymentMethodsUpdate
| Flow::PaymentMethodsDelete
| Flow::NetworkTokenStatusCheck
| Flow::PaymentMethodCollectLink
| Flow::ValidatePaymentMethod
| Flow::ListCountriesCurrencies

View File

@ -1559,3 +1559,37 @@ pub async fn payment_method_session_delete_saved_payment_method(
))
.await
}
#[cfg(feature = "v2")]
#[instrument(skip_all, fields(flow = ?Flow::NetworkTokenStatusCheck))]
pub async fn network_token_status_check_api(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<id_type::GlobalPaymentMethodId>,
) -> HttpResponse {
let flow = Flow::NetworkTokenStatusCheck;
let payment_method_id = path.into_inner();
Box::pin(api::server_wrap(
flow,
state,
&req,
payment_method_id,
|state, auth: auth::AuthenticationData, payment_method_id, _| {
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store),
));
payment_methods_routes::check_network_token_status(
state,
merchant_context,
payment_method_id,
)
},
&auth::V2ApiKeyAuth {
is_connected_allowed: false,
is_platform_allowed: false,
},
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -344,24 +344,23 @@ pub struct CheckTokenStatus {
pub customer_id: id_type::GlobalCustomerId,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub enum TokenStatus {
Active,
Inactive,
Suspended,
Deactivated,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckTokenStatusResponsePayload {
pub struct CheckTokenStatusResponse {
pub token_status: TokenStatus,
pub token_expiry_month: Secret<String>,
pub token_expiry_year: Secret<String>,
pub token_status: TokenStatus,
}
#[derive(Debug, Deserialize)]
pub struct CheckTokenStatusResponse {
pub payload: CheckTokenStatusResponsePayload,
pub card_last_4: String,
pub card_expiry: String,
pub token_last_4: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]

View File

@ -140,6 +140,8 @@ pub enum Flow {
PaymentMethodsUpdate,
/// Payment methods delete flow.
PaymentMethodsDelete,
/// Network token status check flow.
NetworkTokenStatusCheck,
/// Default Payment method flow.
DefaultPaymentMethodsSet,
/// Payments create flow.