diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 11cd3535e41..d16cb6e1f49 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -4052,175 +4052,34 @@ pub async fn update_payment_method_core( request: api::PaymentMethodUpdate, payment_method_id: &id_type::GlobalPaymentMethodId, ) -> RouterResult { - let db = state.store.as_ref(); + let mut handler = pm_types::PaymentMethodUpdateHandler::generate( + state, + platform, + profile, + request, + payment_method_id, + ) + .await + .attach_printable("Failed to generate PaymentMethodUpdateHandler")?; - let payment_method = db - .find_payment_method( - platform.get_provider().get_key_store(), - payment_method_id, - platform.get_provider().get_account().storage_scheme, - ) + let card_cvc_details = handler + .update_cvc_if_required() .await - .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; - - let current_vault_id = payment_method.locker_id.clone(); + .attach_printable("Failed to update card cvc")?; - when( - payment_method.status == enums::PaymentMethodStatus::AwaitingData, - || { - Err(errors::ApiErrorResponse::InvalidRequestData { - message: "This Payment method is awaiting data and hence cannot be updated" - .to_string(), - }) - }, - )?; - - // Stage 1: Update CVC in redis if provided - let card_cvc = request.fetch_card_cvc_update(); - let card_cvc_token_details = card_cvc - .async_map(|cvc| { - vault::insert_cvc_using_payment_token( - state, - payment_method_id, - cvc, - common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME, - platform.get_provider().get_key_store(), - ) - }) + let (vaulting_data, vaulting_resp) = handler + .perform_vaulting_operations_if_required() .await - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to insert encrypted cvc in redis")?; - - // Stage 2: Update payment method if required - let updated_payment_method = if request.is_payment_method_update_required() { - let (vault_request_data, vault_id, fingerprint_id) = if request - .is_payment_method_metadata_update() - { - let pmd: domain::PaymentMethodVaultingData = vault::retrieve_payment_method_from_vault( - state, - platform, - profile, - &payment_method, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to retrieve payment method from vault")? - .data; - - let vault_request_data = - request - .payment_method_data - .clone() - .map(|payment_method_data| { - pm_transforms::generate_pm_vaulting_req_from_update_request( - pmd, - payment_method_data, - ) - }); + .attach_printable("Failed to perform vaulting operations for payment method update")?; - match vault_request_data { - // cannot use async map because of problems related to lifetimes - // to overcome this, we will have to use a move closure and add some clones - Some(ref vault_request_data_req) => { - let fingerprint_id_from_vault = vault::get_fingerprint_id_from_vault( - state, - vault_request_data_req, - payment_method - .customer_id - .clone() - .get_required_value("GlobalCustomerId")? - .get_string_repr() - .to_owned(), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get fingerprint_id from vault")?; - - let is_stored_payment_method_same_as_in_request = payment_method - .locker_fingerprint_id - .clone() - .map(|data| data == fingerprint_id_from_vault); - - // If payment method with the same fingerprint exists, return it as an idempotent response - match is_stored_payment_method_same_as_in_request { - Some(true) => { - logger::info!("Payment method with the same fingerprint_id already exists, returning existing payment method data"); - (None, None, None) - } - Some(false) | None => { - logger::info!("No existing payment method with the same fingerprint_id, proceeding to vault the payment method"); - let (vault_response, _) = vault_payment_method( - state, - vault_request_data_req, - platform, - profile, - current_vault_id, - fingerprint_id_from_vault, - &payment_method - .customer_id - .clone() - .get_required_value("GlobalCustomerId")?, - ) - .await - .attach_printable("Failed to add payment method in vault")?; - - let vault_id = vault_response.vault_id.get_string_repr().to_owned(); - ( - vault_request_data, - Some(vault_id), - vault_response.fingerprint_id, - ) - } - } - } - None => (None, None, None), - } - } else { - (None, None, None) - }; - - let pm_update = create_pm_additional_data_update( - vault_request_data.as_ref(), - state, - platform.get_provider().get_key_store(), - vault_id, - fingerprint_id, - &payment_method, - request.connector_token_details, - request.network_transaction_id, - None, - None, - None, - None, - None, - ) + handler + .update_payment_method_if_required(vaulting_data, vaulting_resp) .await - .attach_printable("Unable to create Payment method data")?; - - db.update_payment_method( - platform.get_provider().get_key_store(), - payment_method, - pm_update, - platform.get_provider().get_account().storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update payment method in db")? - } else { - payment_method - }; - - let response = pm_transforms::generate_payment_method_response( - &updated_payment_method, - &None, - Some(common_enums::StorageType::Persistent), - card_cvc_token_details, - updated_payment_method.customer_id.clone(), - None, - )?; + .attach_printable("Failed to update payment method in db")?; - // Add a PT task to handle payment_method delete from vault + let response = handler + .generate_response(card_cvc_details) + .attach_printable("Failed to generate response for payment method update")?; Ok(response) } @@ -4662,7 +4521,7 @@ pub async fn payment_methods_session_update_payment_method( .attach_printable("Failed to retrieve payment methods session from db")?; // Get the associated_pm_token_details for the payment_method_token from the request - let associated_pm_token_details = payment_method_session + payment_method_session .associated_payment_methods .as_ref() .and_then(|payment_methods| { @@ -4681,9 +4540,21 @@ pub async fn payment_methods_session_update_payment_method( .await .attach_printable("Failed to retrieve payment method id from payment method token data")?; + // Insert the token as the first element in the associated payment methods list + let mut tokens = payment_method_session + .associated_payment_methods + .clone() + .map(|tokens| tokens.into_iter().filter(|token| match &token.payment_method_token { + common_types::payment_methods::AssociatedPaymentMethodTokenType::PaymentMethodSessionToken(token) => token != &request.payment_method_token + }).collect::>()); + + tokens.as_mut().map(|tokens| tokens.insert(0, common_types::payment_methods::AssociatedPaymentMethods { + payment_method_token: common_types::payment_methods::AssociatedPaymentMethodTokenType::PaymentMethodSessionToken(request.payment_method_token.clone()), + })); + // Update payment method session with associated payment methods let update_payment_method_session = hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum::UpdateAssociatedPaymentMethods { - associated_payment_methods: Some(vec![associated_pm_token_details.clone()]) + associated_payment_methods: tokens }; let updated_payment_method_session = db @@ -4733,15 +4604,16 @@ pub async fn payment_methods_session_delete_payment_method( let db = state.store.as_ref(); // Validate if the session still exists - db.get_payment_methods_session( - platform.get_provider().get_key_store(), - &payment_method_session_id, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { - message: "payment methods session does not exist or has expired".to_string(), - }) - .attach_printable("Failed to retrieve payment methods session from db")?; + let payment_method_session = db + .get_payment_methods_session( + platform.get_provider().get_key_store(), + &payment_method_session_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment methods session does not exist or has expired".to_string(), + }) + .attach_printable("Failed to retrieve payment methods session from db")?; let payment_method_id = utils::retrieve_payment_method_id_from_payment_method_token_data(&state, pm_token.clone()) @@ -4754,6 +4626,28 @@ pub async fn payment_methods_session_delete_payment_method( .await .attach_printable("Failed to delete saved payment method")?; + let tokens = payment_method_session + .associated_payment_methods + .clone() + .map(|tokens| tokens.into_iter().filter(|token| match &token.payment_method_token { + common_types::payment_methods::AssociatedPaymentMethodTokenType::PaymentMethodSessionToken(token) => token != &pm_token + }).collect::>()); + + // Update payment method session with associated payment methods + let update_payment_method_session = hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum::UpdateAssociatedPaymentMethods { + associated_payment_methods: tokens + }; + + db.update_payment_method_session( + platform.get_provider().get_key_store(), + &payment_method_session_id, + update_payment_method_session, + payment_method_session, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method session with associated payment methods")?; + Ok(services::ApplicationResponse::Json( payment_methods::PaymentMethodDeleteSessionResponse { payment_method_token: pm_token, @@ -5412,3 +5306,224 @@ pub async fn payment_method_get_token_details_core( Err(e) => Err(e), } } + +#[cfg(feature = "v2")] +impl<'a> pm_types::PaymentMethodUpdateHandler<'a> { + pub async fn generate( + state: &'a SessionState, + platform: &'a domain::Platform, + profile: &'a domain::Profile, + request: api::PaymentMethodUpdate, + payment_method_id: &'a id_type::GlobalPaymentMethodId, + ) -> RouterResult { + let db = state.store.as_ref(); + + let payment_method = db + .find_payment_method( + platform.get_provider().get_key_store(), + payment_method_id, + platform.get_provider().get_account().storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let handler = Self { + platform, + profile, + request, + payment_method, + state, + }; + + handler.validate()?; + + Ok(handler) + } + + fn validate(&self) -> RouterResult<()> { + let payment_method = &self.payment_method; + when( + payment_method.status != enums::PaymentMethodStatus::Active, + || { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Only Active Payment Methods can be updated".to_string(), + }) + }, + )?; + + Ok(()) + } + + pub async fn update_cvc_if_required( + &self, + ) -> RouterResult> { + let card_cvc = self.request.fetch_card_cvc_update(); + let card_cvc_token_details = card_cvc + .async_map(|cvc| { + vault::insert_cvc_using_payment_token( + self.state, + self.payment_method.get_id(), + cvc, + common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME, + self.platform.get_provider().get_key_store(), + ) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to insert encrypted cvc in redis")?; + + Ok(card_cvc_token_details) + } + + pub async fn perform_vaulting_operations_if_required( + &self, + ) -> RouterResult<( + Option, + Option, + )> { + if !self.request.is_payment_method_metadata_update() { + return Ok((None, None)); + } + + let pmd: domain::PaymentMethodVaultingData = vault::retrieve_payment_method_from_vault( + self.state, + self.platform, + self.profile, + &self.payment_method, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to retrieve payment method from vault")? + .data; + + let vault_request_data = self + .request + .payment_method_data + .clone() + .map(|payment_method_data| { + pm_transforms::generate_pm_vaulting_req_from_update_request( + pmd, + payment_method_data, + ) + }) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "payment_method_data", + })?; + + let current_vault_id = self.payment_method.locker_id.clone(); + + let fingerprint_id_from_vault = vault::get_fingerprint_id_from_vault( + self.state, + &vault_request_data, + self.payment_method + .customer_id + .clone() + .get_required_value("GlobalCustomerId")? + .get_string_repr() + .to_owned(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get fingerprint_id from vault")?; + + let is_stored_payment_method_same_as_in_request = self + .payment_method + .locker_fingerprint_id + .clone() + .map(|data| data == fingerprint_id_from_vault); + + match is_stored_payment_method_same_as_in_request { + Some(true) => { + logger::info!( + "Payment method vault data is same as in request, skipping vault update" + ); + Ok((None, None)) + } + Some(false) | None => { + logger::info!("Payment method vault data is different from request, proceeding with vault update"); + let (vault_response, _) = vault_payment_method( + self.state, + &vault_request_data, + self.platform, + self.profile, + current_vault_id, + fingerprint_id_from_vault, + &self + .payment_method + .customer_id + .to_owned() + .get_required_value("GlobalCustomerId")?, + ) + .await + .attach_printable("Failed to add payment method in vault")?; + + Ok((Some(vault_request_data), Some(vault_response))) + } + } + } + + pub async fn update_payment_method_if_required( + &mut self, + vault_request_data: Option, + vault_resp: Option, + ) -> RouterResult<()> { + if !self.request.is_payment_method_update_required() { + return Ok(()); + } + + let db = self.state.store.as_ref(); + + let pm_update = create_pm_additional_data_update( + vault_request_data.as_ref(), + self.state, + self.platform.get_provider().get_key_store(), + vault_resp + .as_ref() + .map(|resp| resp.vault_id.get_string_repr().to_owned()), + vault_resp + .as_ref() + .and_then(|resp| resp.fingerprint_id.clone()), + &self.payment_method, + self.request.connector_token_details.clone(), + self.request.network_transaction_id.clone(), + None, + None, + None, + None, + None, + ) + .await + .attach_printable("Unable to create Payment method data")?; + + let updated_payment_method = db + .update_payment_method( + self.platform.get_provider().get_key_store(), + self.payment_method.clone(), + pm_update, + self.platform.get_provider().get_account().storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + + self.payment_method = updated_payment_method; + Ok(()) + } + + pub fn generate_response( + &self, + card_cvc_token_details: Option, + ) -> RouterResult { + let response = pm_transforms::generate_payment_method_response( + &self.payment_method, + &None, + Some(common_enums::StorageType::Persistent), + card_cvc_token_details, + self.payment_method.customer_id.clone(), + None, + )?; + + Ok(response) + } +} diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs index 97d6ba9f4ff..6d337b1cf55 100644 --- a/crates/router/src/types/payment_methods.rs +++ b/crates/router/src/types/payment_methods.rs @@ -429,3 +429,12 @@ pub struct NetworkTokenMetaDataUpdateBody { pub struct PanMetadataUpdateBody { pub card: NetworkTokenRequestorData, } + +#[cfg(feature = "v2")] +pub struct PaymentMethodUpdateHandler<'a> { + pub platform: &'a hyperswitch_domain_models::platform::Platform, + pub profile: &'a hyperswitch_domain_models::business_profile::Profile, + pub request: api_models::payment_methods::PaymentMethodUpdate, + pub payment_method: hyperswitch_domain_models::payment_methods::PaymentMethod, + pub state: &'a crate::routes::app::SessionState, +}