mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(payment-methods): create payment_token in vault confirm / do payment-confirm with temp token from session (#8525)
This commit is contained in:
@ -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")]
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user