diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index b8f4a9d6d9..59e65c0605 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -8,9 +8,10 @@ use crate::{ PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ - PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, - PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, - PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsExternalAuthenticationRequest, + ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints, + PaymentListFilterConstraints, PaymentListFilters, PaymentListFiltersV2, + PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, + PaymentsCaptureRequest, PaymentsExternalAuthenticationRequest, PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, @@ -201,3 +202,5 @@ impl ApiEventMetric for PaymentsExternalAuthenticationRequest { }) } } + +impl ApiEventMetric for ExtendedCardInfoResponse {} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 5d35ba3e49..bd19443d00 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4622,6 +4622,12 @@ pub enum PaymentLinkStatusWrap { IntentStatus(api_enums::IntentStatus), } +#[derive(Debug, Default, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct ExtendedCardInfoResponse { + // Encrypted customer payment method data + pub payload: String, +} + #[cfg(test)] mod payments_request_api_contract { #![allow(clippy::unwrap_used)] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index bb9ef73199..810143d115 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -470,6 +470,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, + api_models::payments::ExtendedCardInfoResponse, api_models::routing::RoutingConfigRequest, api_models::routing::RoutingDictionaryRecord, api_models::routing::RoutingKind, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 580eb4a80b..bca1cbb64b 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -262,6 +262,8 @@ pub enum StripeErrorCode { CurrencyConversionFailed, #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_25", message = "Cannot delete the default payment method")] PaymentMethodDeleteFailed, + #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "Extended card info does not exist")] + ExtendedCardInfoNotFound, // [#216]: https://github.com/juspay/hyperswitch/issues/216 // Implement the remaining stripe error codes @@ -643,6 +645,7 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::InvalidWalletToken { wallet_name } => { Self::InvalidWalletToken { wallet_name } } + errors::ApiErrorResponse::ExtendedCardInfoNotFound => Self::ExtendedCardInfoNotFound, } } } @@ -714,7 +717,8 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::PaymentMethodUnactivated | Self::InvalidConnectorConfiguration { .. } | Self::CurrencyConversionFailed - | Self::PaymentMethodDeleteFailed => StatusCode::BAD_REQUEST, + | Self::PaymentMethodDeleteFailed + | Self::ExtendedCardInfoNotFound => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::PayoutFailed | Self::PaymentLinkNotFound diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 5d1e527d6d..0c492f0f08 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -269,6 +269,8 @@ pub enum ApiErrorResponse { message = "Invalid Cookie" )] InvalidCookie, + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_27", message = "Extended card info does not exist")] + ExtendedCardInfoNotFound, } impl PTError for ApiErrorResponse { diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 1197e01cdf..880c0d7b20 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -301,6 +301,9 @@ impl ErrorSwitch for ApiErrorRespon Self::InvalidCookie => { AER::BadRequest(ApiError::new("IR", 26, "Invalid Cookie", None)) } + Self::ExtendedCardInfoNotFound => { + AER::NotFound(ApiError::new("IR", 27, "Extended card info does not exist", None)) + } } } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index aa049cb6c6..e2a5f14844 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -4034,3 +4034,26 @@ pub async fn payment_external_authentication( }, )) } + +#[instrument(skip_all)] +pub async fn get_extended_card_info( + state: AppState, + merchant_id: String, + payment_id: String, +) -> RouterResponse { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let key = helpers::get_redis_key_for_extended_card_info(&merchant_id, &payment_id); + let payload = redis_conn + .get_key::(&key) + .await + .change_context(errors::ApiErrorResponse::ExtendedCardInfoNotFound)?; + + Ok(services::ApplicationResponse::Json( + payments_api::ExtendedCardInfoResponse { payload }, + )) +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index bcc911e497..ff69749405 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4319,3 +4319,7 @@ pub fn validate_mandate_data_and_future_usage( Ok(()) } } + +pub fn get_redis_key_for_extended_card_info(merchant_id: &str, payment_id: &str) -> String { + format!("{merchant_id}_{payment_id}_extended_card_info") +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 595fd26f92..d42d3fe003 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -382,6 +382,9 @@ impl Payments { ) .service( web::resource("/{payment_id}/3ds/authentication").route(web::post().to(payments_external_authentication)), + ) + .service( + web::resource("/{payment_id}/extended_card_info").route(web::get().to(retrieve_extended_card_info)), ); } route diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index de87f7fce5..f8a0a6a5ff 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -120,7 +120,8 @@ impl From for ApiIdentifier { | Flow::PaymentsRedirect | Flow::PaymentsIncrementalAuthorization | Flow::PaymentsExternalAuthentication - | Flow::PaymentsAuthorize => Self::Payments, + | Flow::PaymentsAuthorize + | Flow::GetExtendedCardInfo => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index c1d67c4ce0..c9e1cf14ce 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1357,6 +1357,30 @@ pub async fn post_3ds_payments_authorize( .await } +/// Retrieve endpoint for merchant to fetch the encrypted customer payment method data +#[instrument(skip_all, fields(flow = ?Flow::GetExtendedCardInfo, payment_id))] +pub async fn retrieve_extended_card_info( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path, +) -> impl Responder { + let flow = Flow::GetExtendedCardInfo; + let payment_id = path.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payment_id, + |state, auth, payment_id, _| { + payments::get_extended_card_info(state, auth.merchant_account.merchant_id, payment_id) + }, + &auth::ApiKeyAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub fn get_or_generate_payment_id( payload: &mut payment_types::PaymentsRequest, ) -> errors::RouterResult<()> { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 9dfdb6a57a..a7aac03db5 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -402,6 +402,8 @@ pub enum Flow { RetrievePollStatus, /// Toggles the extended card info feature in profile level ToggleExtendedCardInfo, + /// Get the extended card info associated to a payment_id + GetExtendedCardInfo, } /// diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index bd4d02cf4b..9d9d96ccac 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8707,6 +8707,17 @@ "mandate_revoked" ] }, + "ExtendedCardInfoResponse": { + "type": "object", + "required": [ + "payload" + ], + "properties": { + "payload": { + "type": "string" + } + } + }, "ExternalAuthenticationDetailsResponse": { "type": "object", "required": [