diff --git a/api-reference/docs.json b/api-reference/docs.json index b6f778eb08..c0418b1123 100644 --- a/api-reference/docs.json +++ b/api-reference/docs.json @@ -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": [ diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index bf4404b607..c51a16b86a 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -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": [ diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 45ce4f46d0..ed1bf12a24 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -492,3 +492,29 @@ impl From 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, +} diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index cfb45ef045..65c3f7127b 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -3262,3 +3262,56 @@ pub struct AuthenticationDetails { #[schema(value_type = Option)] pub error: Option, } + +#[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, + + /// The expiry year of the network token if active + #[schema(value_type = String)] + pub token_expiry_year: masking::Secret, + + /// 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), +} diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 4e4bb85e4a..5224635dea 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -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, diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index 3f0ed7d799..ed4905ed53 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -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 diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index f1f5c482c3..578302d7e7 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -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 { + 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 { + // 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, + )) +} diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index 1a5d328303..6cecd373cb 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -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>, Option>), errors::NetworkTokenizationError> -{ - todo!() + state: &routes::SessionState, + customer_id: &id_type::GlobalCustomerId, + network_token_requestor_reference_id: String, + tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult { + 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 { + 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")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 43e1deb443..52c9d05d64 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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( diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index da8a2a23ec..f138e18377 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -126,6 +126,7 @@ impl From for ApiIdentifier { | Flow::PaymentMethodsRetrieve | Flow::PaymentMethodsUpdate | Flow::PaymentMethodsDelete + | Flow::NetworkTokenStatusCheck | Flow::PaymentMethodCollectLink | Flow::ValidatePaymentMethod | Flow::ListCountriesCurrencies diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 7aa1ba949b..61b1f4f473 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -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, + req: HttpRequest, + path: web::Path, +) -> 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 +} diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs index 45ee498967..bf3630786d 100644 --- a/crates/router/src/types/payment_methods.rs +++ b/crates/router/src/types/payment_methods.rs @@ -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, pub token_expiry_year: Secret, - 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)] diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 5366efcf36..3bddc05b49 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -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.