feat(payment-methods): create payment_token in vault confirm / do payment-confirm with temp token from session (#8525)

This commit is contained in:
Sakil Mostak
2025-07-07 16:52:33 +05:30
committed by GitHub
parent 94e3905177
commit 4aca45531b
6 changed files with 182 additions and 8 deletions

View File

@ -3040,7 +3040,7 @@ pub struct PaymentMethodSessionResponse {
/// The payment method that was created using this payment method session
#[schema(value_type = Option<Vec<String>>)]
pub associated_payment_methods: Option<Vec<id_type::GlobalPaymentMethodId>>,
pub associated_payment_methods: Option<Vec<String>>,
/// The token-id created if there is tokenization_data present
#[schema(value_type = Option<String>, example = "12345_tok_01926c58bc6e77c09e809964e72af8c8")]

View File

@ -12,7 +12,7 @@ pub struct PaymentMethodSession {
pub return_url: Option<common_utils::types::Url>,
#[serde(with = "common_utils::custom_serde::iso8601")]
pub expires_at: time::PrimitiveDateTime,
pub associated_payment_methods: Option<Vec<common_utils::id_type::GlobalPaymentMethodId>>,
pub associated_payment_methods: Option<Vec<String>>,
pub associated_payment: Option<common_utils::id_type::GlobalPaymentId>,
pub associated_token_id: Option<common_utils::id_type::GlobalTokenId>,
}

View File

@ -612,7 +612,7 @@ pub struct PaymentMethodSession {
pub network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
pub tokenization_data: Option<pii::SecretSerdeValue>,
pub expires_at: PrimitiveDateTime,
pub associated_payment_methods: Option<Vec<id_type::GlobalPaymentMethodId>>,
pub associated_payment_methods: Option<Vec<String>>,
pub associated_payment: Option<id_type::GlobalPaymentId>,
pub associated_token_id: Option<id_type::GlobalTokenId>,
}
@ -850,11 +850,14 @@ pub trait PaymentMethodInterface {
#[cfg(feature = "v2")]
pub enum PaymentMethodsSessionUpdateEnum {
GeneralUpdate {
billing: Option<Encryptable<Address>>,
billing: Box<Option<Encryptable<Address>>>,
psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
tokenization_data: Option<pii::SecretSerdeValue>,
},
UpdateAssociatedPaymentMethods {
associated_payment_methods: Option<Vec<String>>,
},
}
#[cfg(feature = "v2")]
@ -867,10 +870,20 @@ impl From<PaymentMethodsSessionUpdateEnum> for PaymentMethodsSessionUpdateIntern
network_tokenization,
tokenization_data,
} => Self {
billing,
billing: *billing,
psp_tokenization,
network_tokenization,
tokenization_data,
associated_payment_methods: None,
},
PaymentMethodsSessionUpdateEnum::UpdateAssociatedPaymentMethods {
associated_payment_methods,
} => Self {
billing: None,
psp_tokenization: None,
network_tokenization: None,
tokenization_data: None,
associated_payment_methods,
},
}
}
@ -901,7 +914,9 @@ impl PaymentMethodSession {
tokenization_data: update_session.tokenization_data.or(tokenization_data),
expires_at,
return_url,
associated_payment_methods,
associated_payment_methods: update_session
.associated_payment_methods
.or(associated_payment_methods),
associated_payment,
associated_token_id,
}
@ -914,6 +929,7 @@ pub struct PaymentMethodsSessionUpdateInternal {
pub psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
pub network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
pub tokenization_data: Option<pii::SecretSerdeValue>,
pub associated_payment_methods: Option<Vec<String>>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]

View File

@ -2753,7 +2753,7 @@ pub async fn payment_methods_session_update(
let payment_method_session_domain_model =
hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum::GeneralUpdate{
billing,
billing: Box::new(billing),
psp_tokenization: request.psp_tokenization,
network_tokenization: request.network_tokenization,
tokenization_data: request.tokenization_data,
@ -3012,7 +3012,7 @@ pub async fn payment_methods_session_confirm(
)
.attach_printable("Failed to create payment method request")?;
let (payment_method_response, _payment_method) = create_payment_method_core(
let (payment_method_response, payment_method) = create_payment_method_core(
&state,
&req_state,
create_payment_method_request.clone(),
@ -3021,6 +3021,50 @@ pub async fn payment_methods_session_confirm(
)
.await?;
let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token");
let token_data = get_pm_list_token_data(request.payment_method_type, &payment_method)?;
let intent_fulfillment_time = common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME;
// insert the token data into redis
if let Some(token_data) = token_data {
pm_routes::ParentPaymentMethodToken::create_key_for_token((
&parent_payment_method_token,
request.payment_method_type,
))
.insert(intent_fulfillment_time, token_data, &state)
.await?;
};
let update_payment_method_session = hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum::UpdateAssociatedPaymentMethods {
associated_payment_methods: Some(vec![parent_payment_method_token.clone()])
};
vault::insert_cvc_using_payment_token(
&state,
&parent_payment_method_token,
create_payment_method_request.payment_method_data.clone(),
request.payment_method_type,
intent_fulfillment_time,
merchant_context.get_merchant_key_store().key.get_inner(),
)
.await?;
let payment_method_session = db
.update_payment_method_session(
key_manager_state,
merchant_context.get_merchant_key_store(),
&payment_method_session_id,
update_payment_method_session,
payment_method_session,
)
.await
.to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
message: "payment methods session does not exist or has expired".to_string(),
})
.attach_printable("Failed to update payment methods session from db")?;
let payments_response = match &payment_method_session.psp_tokenization {
Some(common_types::payment_methods::PspTokenization {
tokenization_type: common_enums::TokenizationType::MultiUse,

View File

@ -1527,6 +1527,111 @@ pub async fn retrieve_payment_method_from_vault_using_payment_token(
Ok((payment_method, vault_data))
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TemporaryVaultCvc {
card_cvc: masking::Secret<String>,
}
#[cfg(feature = "v2")]
#[instrument(skip_all)]
pub async fn insert_cvc_using_payment_token(
state: &routes::SessionState,
payment_token: &String,
payment_method_data: api_models::payment_methods::PaymentMethodCreateData,
payment_method: common_enums::PaymentMethod,
fullfillment_time: i64,
encryption_key: &masking::Secret<Vec<u8>>,
) -> RouterResult<()> {
let card_cvc = domain::PaymentMethodVaultingData::from(payment_method_data)
.get_card()
.and_then(|card| card.card_cvc.clone());
if let Some(card_cvc) = card_cvc {
let redis_conn = state
.store
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")?;
let key = format!(
"pm_token_{}_{}_hyperswitch_cvc",
payment_token, payment_method
);
let payload_to_be_encrypted = TemporaryVaultCvc { card_cvc };
let payload = payload_to_be_encrypted
.encode_to_string_of_json()
.change_context(errors::ApiErrorResponse::InternalServerError)?;
// Encrypt the CVC and store it in Redis
let encrypted_payload = GcmAes256
.encode_message(encryption_key.peek().as_ref(), payload.as_bytes())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encode TemporaryVaultCvc for vault")?;
redis_conn
.set_key_if_not_exists_with_expiry(
&key.as_str().into(),
bytes::Bytes::from(encrypted_payload),
Some(fullfillment_time),
)
.await
.change_context(errors::StorageError::KVError)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to add token in redis")?;
};
Ok(())
}
#[cfg(feature = "v2")]
#[instrument(skip_all)]
pub async fn retrieve_and_delete_cvc_from_payment_token(
state: &routes::SessionState,
payment_token: &String,
payment_method: common_enums::PaymentMethod,
encryption_key: &masking::Secret<Vec<u8>>,
) -> RouterResult<masking::Secret<String>> {
let redis_conn = state
.store
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")?;
let key = format!(
"pm_token_{}_{}_hyperswitch_cvc",
payment_token, payment_method
);
let data = redis_conn
.get_key::<bytes::Bytes>(&key.clone().into())
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch the token from redis")?;
// decrypt the cvc data
let decrypted_payload = GcmAes256
.decode_message(
encryption_key.peek().as_ref(),
masking::Secret::new(data.into()),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to decode TemporaryVaultCvc from vault")?;
let cvc_data: TemporaryVaultCvc = bytes::Bytes::from(decrypted_payload)
.parse_struct("TemporaryVaultCvc")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to deserialize TemporaryVaultCvc")?;
// delete key after retrieving the cvc
redis_conn.delete_key(&key.into()).await.map_err(|err| {
logger::error!("Failed to delete token from redis: {:?}", err);
});
Ok(cvc_data.card_cvc)
}
#[cfg(feature = "v2")]
#[instrument(skip_all)]
pub async fn delete_payment_token(

View File

@ -415,6 +415,15 @@ impl<F: Clone + Send + Sync> Domain<F, PaymentsConfirmIntentRequest, PaymentConf
.ok_or(errors::ApiErrorResponse::InvalidDataValue {
field_name: "card_cvc",
})
.or(
payment_methods::vault::retrieve_and_delete_cvc_from_payment_token(
state,
payment_token,
payment_data.payment_attempt.payment_method_type,
merchant_context.get_merchant_key_store().key.get_inner(),
)
.await,
)
.attach_printable("card_cvc not provided")?,
card_token.card_holder_name.clone(),
)