Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(payment_methods_session_v2): update flow of Payment Method Session #7157

Open
wants to merge 5 commits into
base: payment_methods_session
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions api-reference-v2/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -14585,6 +14585,35 @@
}
}
},
"PaymentMethodsSessionUpdateRequest": {
"type": "object",
"properties": {
"billing": {
"allOf": [
{
"$ref": "#/components/schemas/Address"
}
],
"nullable": true
},
"psp_tokenization": {
"allOf": [
{
"$ref": "#/components/schemas/PspTokenization"
}
],
"nullable": true
},
"network_tokenization": {
"allOf": [
{
"$ref": "#/components/schemas/NetworkTokenization"
}
],
"nullable": true
}
}
},
"PaymentProcessingDetails": {
"type": "object",
"required": [
Expand Down
3 changes: 3 additions & 0 deletions crates/api_models/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ impl ApiEventMetric for DisputeListFilters {
#[cfg(feature = "v2")]
impl ApiEventMetric for PaymentMethodSessionRequest {}

#[cfg(feature = "v2")]
impl ApiEventMetric for PaymentMethodsSessionUpdateRequest {}

#[cfg(feature = "v2")]
impl ApiEventMetric for PaymentMethodsSessionResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Expand Down
16 changes: 16 additions & 0 deletions crates/api_models/src/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2402,6 +2402,22 @@ pub struct PaymentMethodSessionRequest {
pub expires_in: Option<u32>,
}

#[cfg(feature = "v2")]
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct PaymentMethodsSessionUpdateRequest {
/// The billing address details of the customer. This will also be used for any new payment methods added during the session
#[schema(value_type = Option<Address>)]
pub billing: Option<payments::Address>,

/// The tokenization type to be applied
#[schema(value_type = Option<PspTokenization>)]
pub psp_tokenization: Option<common_types::payment_methods::PspTokenization>,

/// The network tokenization configuration if applicable
#[schema(value_type = Option<NetworkTokenization>)]
pub network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
}

#[cfg(feature = "v2")]
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct PaymentMethodSessionUpdateSavedPaymentMethod {
Expand Down
9 changes: 9 additions & 0 deletions crates/diesel_models/src/payment_methods_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ pub struct PaymentMethodsSession {
#[serde(with = "common_utils::custom_serde::iso8601")]
pub expires_at: time::PrimitiveDateTime,
}

#[cfg(feature = "v2")]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PaymentMethodsSessionUpdate {
pub id: common_utils::id_type::GlobalPaymentMethodSessionId,
pub billing: Option<common_utils::encryption::Encryption>,
pub psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
pub network_tokeinzation: Option<common_types::payment_methods::NetworkTokenization>,
}
88 changes: 88 additions & 0 deletions crates/hyperswitch_domain_models/src/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,3 +594,91 @@ impl super::behaviour::Conversion for PaymentMethodsSession {
})
}
}

#[cfg(feature = "v2")]
#[derive(Clone, Debug, router_derive::ToEncryption)]
pub struct PaymentMethodsSessionUpdate {
pub id: common_utils::id_type::GlobalPaymentMethodSessionId,
#[encrypt(ty = Value)]
pub billing: Option<Encryptable<Address>>,
pub psp_tokenization: Option<common_types::payment_methods::PspTokenization>,
pub network_tokenization: Option<common_types::payment_methods::NetworkTokenization>,
}

#[cfg(feature = "v2")]
#[async_trait::async_trait]
impl super::behaviour::Conversion for PaymentMethodsSessionUpdate {
type DstType = diesel_models::payment_methods_session::PaymentMethodsSessionUpdate;
type NewDstType = diesel_models::payment_methods_session::PaymentMethodsSessionUpdate;
async fn convert(self) -> CustomResult<Self::DstType, ValidationError> {
Ok(Self::DstType {
id: self.id,
billing: self.billing.map(|val| val.into()),
psp_tokenization: self.psp_tokenization,
network_tokeinzation: self.network_tokenization,
})
}

async fn convert_back(
state: &keymanager::KeyManagerState,
storage_model: Self::DstType,
key: &Secret<Vec<u8>>,
key_manager_identifier: keymanager::Identifier,
) -> CustomResult<Self, ValidationError>
where
Self: Sized,
{
use common_utils::ext_traits::ValueExt;

async {
let decrypted_data = crypto_operation(
state,
type_name!(Self::DstType),
CryptoOperation::BatchDecrypt(
EncryptedPaymentMethodsSessionUpdate::to_encryptable(
EncryptedPaymentMethodsSessionUpdate {
billing: storage_model.billing,
},
),
),
key_manager_identifier,
key.peek(),
)
.await
.and_then(|val| val.try_into_batchoperation())?;

let data = EncryptedPaymentMethodsSessionUpdate::from_encryptable(decrypted_data)
.change_context(common_utils::errors::CryptoError::DecodingFailed)
.attach_printable("Invalid batch operation data")?;

let billing = data
.billing
.map(|billing| {
billing.deserialize_inner_value(|value| value.parse_value("Address"))
})
.transpose()
.change_context(common_utils::errors::CryptoError::DecodingFailed)
.attach_printable("Error while deserializing Address")?;

Ok::<Self, error_stack::Report<common_utils::errors::CryptoError>>(Self {
id: storage_model.id,
billing,
psp_tokenization: storage_model.psp_tokenization,
network_tokenization: storage_model.network_tokeinzation,
})
}
.await
.change_context(ValidationError::InvalidValue {
message: "Failed while decrypting payment method data".to_string(),
})
}

async fn construct_new(self) -> CustomResult<Self::NewDstType, ValidationError> {
Ok(Self::NewDstType {
id: self.id,
billing: self.billing.map(|val| val.into()),
psp_tokenization: self.psp_tokenization,
network_tokeinzation: self.network_tokenization,
})
}
}
1 change: 1 addition & 0 deletions crates/openapi/src/openapi_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::payment_methods::PaymentMethodCollectLinkResponse,
api_models::payment_methods::PaymentMethodSubtypeSpecificData,
api_models::payment_methods::PaymentMethodSessionRequest,
api_models::payment_methods::PaymentMethodsSessionUpdateRequest,
api_models::payment_methods::PaymentMethodsSessionResponse,
api_models::payments::PaymentsRetrieveResponse,
api_models::refunds::RefundListRequest,
Expand Down
135 changes: 135 additions & 0 deletions crates/router/src/core/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,56 @@ impl EncryptableData for payment_methods::PaymentMethodSessionRequest {
}
}

#[cfg(feature = "v2")]
#[async_trait::async_trait]
impl EncryptableData for payment_methods::PaymentMethodsSessionUpdateRequest {
type Output = hyperswitch_domain_models::payment_methods::DecryptedPaymentMethodsSession;

async fn encrypt_data(
&self,
key_manager_state: &common_utils::types::keymanager::KeyManagerState,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Self::Output> {
use common_utils::types::keymanager::ToEncryptable;

let encrypted_billing_address = self
.billing
.clone()
.map(|address| address.encode_to_value())
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encode billing address")?
.map(Secret::new);

let batch_encrypted_data = domain_types::crypto_operation(
key_manager_state,
common_utils::type_name!(hyperswitch_domain_models::payment_methods::PaymentMethodsSession),
domain_types::CryptoOperation::BatchEncrypt(
hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession::to_encryptable(
hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession {
billing: encrypted_billing_address,
},
),
),
common_utils::types::keymanager::Identifier::Merchant(key_store.merchant_id.clone()),
key_store.key.peek(),
)
.await
.and_then(|val| val.try_into_batchoperation())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while encrypting payment methods session details".to_string())?;

let encrypted_data =
hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession::from_encryptable(
batch_encrypted_data,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while encrypting payment methods session detailss")?;

Ok(encrypted_data)
}
}

#[cfg(feature = "v2")]
pub async fn payment_methods_session_create(
state: SessionState,
Expand Down Expand Up @@ -1872,6 +1922,72 @@ pub async fn payment_methods_session_create(
Ok(services::ApplicationResponse::Json(response))
}

#[cfg(feature = "v2")]
pub async fn payment_methods_session_update(
state: SessionState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
payment_method_session_id: id_type::GlobalPaymentMethodSessionId,
request: payment_methods::PaymentMethodsSessionUpdateRequest,
) -> RouterResponse<payment_methods::PaymentMethodsSessionResponse> {
let db = state.store.as_ref();
let key_manager_state = &(&state).into();

let payment_method_session_state = db
.get_payment_methods_session(key_manager_state, &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 encrypted_data = request
.encrypt_data(key_manager_state, &key_store)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encrypt payment methods session data")?;

let billing = encrypted_data
.billing
.as_ref()
.map(|data| {
data.clone()
.deserialize_inner_value(|value| value.parse_value("Address"))
})
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to decode billing address")?;

let payment_method_session_domain_model =
hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdate {
id: payment_method_session_id.clone(),
billing,
psp_tokenization: request.psp_tokenization,
network_tokenization: request.network_tokenization,
};

let update_state_change = payment_method_session_update_to_current_state(
payment_method_session_domain_model,
payment_method_session_state,
)
.await;

db.update_payment_method_session(
key_manager_state,
&key_store,
&payment_method_session_id,
update_state_change.clone(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update payment methods session in db")?;

let response = payment_methods::PaymentMethodsSessionResponse::foreign_from((
update_state_change,
Secret::new("CLIENT_SECRET_REDACTED".to_string()),
));
Ok(services::ApplicationResponse::Json(response))
}
#[cfg(feature = "v2")]
pub async fn payment_methods_session_retrieve(
state: SessionState,
Expand Down Expand Up @@ -1987,3 +2103,22 @@ impl pm_types::SavedPMLPaymentsInfo {
Ok(())
}
}

#[cfg(feature = "v2")]
pub async fn payment_method_session_update_to_current_state(
update_state: hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdate,
current_state: hyperswitch_domain_models::payment_methods::PaymentMethodsSession,
) -> hyperswitch_domain_models::payment_methods::PaymentMethodsSession {
hyperswitch_domain_models::payment_methods::PaymentMethodsSession {
id: current_state.id,
customer_id: current_state.customer_id,
billing: update_state.billing.or(current_state.billing),
psp_tokenization: update_state
.psp_tokenization
.or(current_state.psp_tokenization),
network_tokenization: update_state
.network_tokenization
.or(current_state.network_tokenization),
expires_at: current_state.expires_at,
}
}
12 changes: 12 additions & 0 deletions crates/router/src/db/kafka_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3924,6 +3924,18 @@ impl db::payment_method_session::PaymentMethodsSessionInterface for KafkaStore {
.get_payment_methods_session(state, key_store, id)
.await
}

async fn update_payment_method_session(
&self,
state: &KeyManagerState,
key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore,
id: &id_type::GlobalPaymentMethodSessionId,
payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSession,
) -> CustomResult<(), errors::StorageError> {
self.diesel_store
.update_payment_method_session(state, key_store, id, payment_methods_session)
.await
}
}

#[async_trait::async_trait]
Expand Down
Loading
Loading