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
|
/// The payment method that was created using this payment method session
|
||||||
#[schema(value_type = Option<Vec<String>>)]
|
#[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
|
/// The token-id created if there is tokenization_data present
|
||||||
#[schema(value_type = Option<String>, example = "12345_tok_01926c58bc6e77c09e809964e72af8c8")]
|
#[schema(value_type = Option<String>, example = "12345_tok_01926c58bc6e77c09e809964e72af8c8")]
|
||||||
|
|||||||
@ -12,7 +12,7 @@ pub struct PaymentMethodSession {
|
|||||||
pub return_url: Option<common_utils::types::Url>,
|
pub return_url: Option<common_utils::types::Url>,
|
||||||
#[serde(with = "common_utils::custom_serde::iso8601")]
|
#[serde(with = "common_utils::custom_serde::iso8601")]
|
||||||
pub expires_at: time::PrimitiveDateTime,
|
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_payment: Option<common_utils::id_type::GlobalPaymentId>,
|
||||||
pub associated_token_id: Option<common_utils::id_type::GlobalTokenId>,
|
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 network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
|
||||||
pub tokenization_data: Option<pii::SecretSerdeValue>,
|
pub tokenization_data: Option<pii::SecretSerdeValue>,
|
||||||
pub expires_at: PrimitiveDateTime,
|
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_payment: Option<id_type::GlobalPaymentId>,
|
||||||
pub associated_token_id: Option<id_type::GlobalTokenId>,
|
pub associated_token_id: Option<id_type::GlobalTokenId>,
|
||||||
}
|
}
|
||||||
@ -850,11 +850,14 @@ pub trait PaymentMethodInterface {
|
|||||||
#[cfg(feature = "v2")]
|
#[cfg(feature = "v2")]
|
||||||
pub enum PaymentMethodsSessionUpdateEnum {
|
pub enum PaymentMethodsSessionUpdateEnum {
|
||||||
GeneralUpdate {
|
GeneralUpdate {
|
||||||
billing: Option<Encryptable<Address>>,
|
billing: Box<Option<Encryptable<Address>>>,
|
||||||
psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
|
psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
|
||||||
network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
|
network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
|
||||||
tokenization_data: Option<pii::SecretSerdeValue>,
|
tokenization_data: Option<pii::SecretSerdeValue>,
|
||||||
},
|
},
|
||||||
|
UpdateAssociatedPaymentMethods {
|
||||||
|
associated_payment_methods: Option<Vec<String>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "v2")]
|
#[cfg(feature = "v2")]
|
||||||
@ -867,10 +870,20 @@ impl From<PaymentMethodsSessionUpdateEnum> for PaymentMethodsSessionUpdateIntern
|
|||||||
network_tokenization,
|
network_tokenization,
|
||||||
tokenization_data,
|
tokenization_data,
|
||||||
} => Self {
|
} => Self {
|
||||||
billing,
|
billing: *billing,
|
||||||
psp_tokenization,
|
psp_tokenization,
|
||||||
network_tokenization,
|
network_tokenization,
|
||||||
tokenization_data,
|
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),
|
tokenization_data: update_session.tokenization_data.or(tokenization_data),
|
||||||
expires_at,
|
expires_at,
|
||||||
return_url,
|
return_url,
|
||||||
associated_payment_methods,
|
associated_payment_methods: update_session
|
||||||
|
.associated_payment_methods
|
||||||
|
.or(associated_payment_methods),
|
||||||
associated_payment,
|
associated_payment,
|
||||||
associated_token_id,
|
associated_token_id,
|
||||||
}
|
}
|
||||||
@ -914,6 +929,7 @@ pub struct PaymentMethodsSessionUpdateInternal {
|
|||||||
pub psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
|
pub psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
|
||||||
pub network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
|
pub network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
|
||||||
pub tokenization_data: Option<pii::SecretSerdeValue>,
|
pub tokenization_data: Option<pii::SecretSerdeValue>,
|
||||||
|
pub associated_payment_methods: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
|||||||
@ -2753,7 +2753,7 @@ pub async fn payment_methods_session_update(
|
|||||||
|
|
||||||
let payment_method_session_domain_model =
|
let payment_method_session_domain_model =
|
||||||
hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum::GeneralUpdate{
|
hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum::GeneralUpdate{
|
||||||
billing,
|
billing: Box::new(billing),
|
||||||
psp_tokenization: request.psp_tokenization,
|
psp_tokenization: request.psp_tokenization,
|
||||||
network_tokenization: request.network_tokenization,
|
network_tokenization: request.network_tokenization,
|
||||||
tokenization_data: request.tokenization_data,
|
tokenization_data: request.tokenization_data,
|
||||||
@ -3012,7 +3012,7 @@ pub async fn payment_methods_session_confirm(
|
|||||||
)
|
)
|
||||||
.attach_printable("Failed to create payment method request")?;
|
.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,
|
&state,
|
||||||
&req_state,
|
&req_state,
|
||||||
create_payment_method_request.clone(),
|
create_payment_method_request.clone(),
|
||||||
@ -3021,6 +3021,50 @@ pub async fn payment_methods_session_confirm(
|
|||||||
)
|
)
|
||||||
.await?;
|
.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 {
|
let payments_response = match &payment_method_session.psp_tokenization {
|
||||||
Some(common_types::payment_methods::PspTokenization {
|
Some(common_types::payment_methods::PspTokenization {
|
||||||
tokenization_type: common_enums::TokenizationType::MultiUse,
|
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))
|
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")]
|
#[cfg(feature = "v2")]
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn delete_payment_token(
|
pub async fn delete_payment_token(
|
||||||
|
|||||||
@ -415,6 +415,15 @@ impl<F: Clone + Send + Sync> Domain<F, PaymentsConfirmIntentRequest, PaymentConf
|
|||||||
.ok_or(errors::ApiErrorResponse::InvalidDataValue {
|
.ok_or(errors::ApiErrorResponse::InvalidDataValue {
|
||||||
field_name: "card_cvc",
|
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")?,
|
.attach_printable("card_cvc not provided")?,
|
||||||
card_token.card_holder_name.clone(),
|
card_token.card_holder_name.clone(),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user