From d32397f060731f51a15634e221117a554b8b3721 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:56:00 +0530 Subject: [PATCH 01/16] refactor(users): Make `profile_id` in the JWT non-optional (#6537) --- crates/diesel_models/src/query/user_role.rs | 7 +-- crates/router/src/core/user.rs | 61 ++++++------------- crates/router/src/core/user_role.rs | 22 +++---- crates/router/src/db/kafka_store.rs | 4 +- crates/router/src/db/user_role.rs | 20 +++--- crates/router/src/services/authentication.rs | 50 +++++---------- .../src/types/domain/user/decision_manager.rs | 2 +- crates/router/src/utils/user.rs | 2 +- 8 files changed, 58 insertions(+), 110 deletions(-) diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index 2496d22447e..ed018cc2381 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -87,7 +87,7 @@ impl UserRole { user_id: String, org_id: id_type::OrganizationId, merchant_id: id_type::MerchantId, - profile_id: Option, + profile_id: id_type::ProfileId, version: UserRoleVersion, ) -> StorageResult { // Checking in user roles, for a user in token hierarchy, only one of the relation will be true, either org level, merchant level or profile level @@ -103,7 +103,6 @@ impl UserRole { .or(dsl::org_id.eq(org_id).and( dsl::merchant_id .eq(merchant_id) - //TODO: In case of None, profile_id = NULL its unexpected behaviour, after V1 profile id will not be option .and(dsl::profile_id.eq(profile_id)), )); @@ -137,7 +136,6 @@ impl UserRole { .or(dsl::org_id.eq(org_id).and( dsl::merchant_id .eq(merchant_id) - //TODO: In case of None, profile_id = NULL its unexpected behaviour, after V1 profile id will not be option .and(dsl::profile_id.eq(profile_id)), )); @@ -160,7 +158,7 @@ impl UserRole { user_id: String, org_id: id_type::OrganizationId, merchant_id: id_type::MerchantId, - profile_id: Option, + profile_id: id_type::ProfileId, version: UserRoleVersion, ) -> StorageResult { // Checking in user roles, for a user in token hierarchy, only one of the relation will be true, either org level, merchant level or profile level @@ -176,7 +174,6 @@ impl UserRole { .or(dsl::org_id.eq(org_id).and( dsl::merchant_id .eq(merchant_id) - //TODO: In case of None, profile_id = NULL its unexpected behaviour, after V1 profile id will not be option .and(dsl::profile_id.eq(profile_id)), )); diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index e0ae1c531a9..bff6205f5db 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -119,9 +119,7 @@ pub async fn get_user_details( org_id: user_from_token.org_id, is_two_factor_auth_setup: user.get_totp_status() == TotpStatus::Set, recovery_codes_left: user.get_recovery_codes().map(|codes| codes.len()), - profile_id: user_from_token - .profile_id - .ok_or(UserErrors::JwtProfileIdMissing)?, + profile_id: user_from_token.profile_id, entity_type: role_info.get_entity_type(), }, )) @@ -603,7 +601,7 @@ async fn handle_existing_user_invitation( invitee_user_from_db.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V1, ) .await @@ -619,7 +617,7 @@ async fn handle_existing_user_invitation( invitee_user_from_db.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V2, ) .await @@ -673,10 +671,6 @@ async fn handle_existing_user_invitation( .await? } EntityType::Profile => { - let profile_id = user_from_token - .profile_id - .clone() - .ok_or(UserErrors::InternalServerError)?; user_role .add_entity(domain::ProfileLevel { tenant_id: user_from_token @@ -685,7 +679,7 @@ async fn handle_existing_user_invitation( .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), merchant_id: user_from_token.merchant_id.clone(), - profile_id: profile_id.clone(), + profile_id: user_from_token.profile_id.clone(), }) .insert_in_v2(state) .await? @@ -705,16 +699,10 @@ async fn handle_existing_user_invitation( entity_id: user_from_token.merchant_id.get_string_repr().to_owned(), entity_type: EntityType::Merchant, }, - EntityType::Profile => { - let profile_id = user_from_token - .profile_id - .clone() - .ok_or(UserErrors::InternalServerError)?; - email_types::Entity { - entity_id: profile_id.get_string_repr().to_owned(), - entity_type: EntityType::Profile, - } - } + EntityType::Profile => email_types::Entity { + entity_id: user_from_token.profile_id.get_string_repr().to_owned(), + entity_type: EntityType::Profile, + }, }; let email_contents = email_types::InviteUser { @@ -812,10 +800,6 @@ async fn handle_new_user_invitation( .await? } EntityType::Profile => { - let profile_id = user_from_token - .profile_id - .clone() - .ok_or(UserErrors::InternalServerError)?; user_role .add_entity(domain::ProfileLevel { tenant_id: user_from_token @@ -824,7 +808,7 @@ async fn handle_new_user_invitation( .unwrap_or(state.tenant.tenant_id.clone()), org_id: user_from_token.org_id.clone(), merchant_id: user_from_token.merchant_id.clone(), - profile_id: profile_id.clone(), + profile_id: user_from_token.profile_id.clone(), }) .insert_in_v2(state) .await? @@ -848,16 +832,10 @@ async fn handle_new_user_invitation( entity_id: user_from_token.merchant_id.get_string_repr().to_owned(), entity_type: EntityType::Merchant, }, - EntityType::Profile => { - let profile_id = user_from_token - .profile_id - .clone() - .ok_or(UserErrors::InternalServerError)?; - email_types::Entity { - entity_id: profile_id.get_string_repr().to_owned(), - entity_type: EntityType::Profile, - } - } + EntityType::Profile => email_types::Entity { + entity_id: user_from_token.profile_id.get_string_repr().to_owned(), + entity_type: EntityType::Profile, + }, }; let email_contents = email_types::InviteUser { @@ -887,7 +865,7 @@ async fn handle_new_user_invitation( merchant_id: user_from_token.merchant_id.clone(), org_id: user_from_token.org_id.clone(), role_id: request.role_id.clone(), - profile_id: None, + profile_id: user_from_token.profile_id.clone(), tenant_id: user_from_token.tenant_id.clone(), }; @@ -939,7 +917,7 @@ pub async fn resend_invite( user.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V2, ) .await @@ -962,7 +940,7 @@ pub async fn resend_invite( user.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V1, ) .await @@ -1235,10 +1213,7 @@ pub async fn list_user_roles_details( merchant_id: (requestor_role_info.get_entity_type() <= EntityType::Merchant) .then_some(&user_from_token.merchant_id), profile_id: (requestor_role_info.get_entity_type() <= EntityType::Profile) - .then_some(&user_from_token.profile_id) - .cloned() - .flatten() - .as_ref(), + .then_some(&user_from_token.profile_id), entity_id: None, version: None, status: None, @@ -2865,7 +2840,7 @@ pub async fn switch_profile_for_user_in_org_and_merchant( request: user_api::SwitchProfileRequest, user_from_token: auth::UserFromToken, ) -> UserResponse { - if user_from_token.profile_id == Some(request.profile_id.clone()) { + if user_from_token.profile_id == request.profile_id { return Err(UserErrors::InvalidRoleOperationWithMessage( "User switching to same profile".to_string(), ) diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 95a8b7d51d1..6641e553fd8 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -161,7 +161,7 @@ pub async fn update_user_role( user_to_be_updated.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V2, ) .await @@ -215,7 +215,7 @@ pub async fn update_user_role( user_to_be_updated.get_user_id(), &user_from_token.org_id, Some(&user_from_token.merchant_id), - user_from_token.profile_id.as_ref(), + Some(&user_from_token.profile_id), UserRoleUpdate::UpdateRole { role_id: req.role_id.clone(), modified_by: user_from_token.user_id.clone(), @@ -234,7 +234,7 @@ pub async fn update_user_role( user_to_be_updated.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V1, ) .await @@ -288,7 +288,7 @@ pub async fn update_user_role( user_to_be_updated.get_user_id(), &user_from_token.org_id, Some(&user_from_token.merchant_id), - user_from_token.profile_id.as_ref(), + Some(&user_from_token.profile_id), UserRoleUpdate::UpdateRole { role_id: req.role_id.clone(), modified_by: user_from_token.user_id, @@ -475,7 +475,7 @@ pub async fn delete_user_role( user_from_db.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V2, ) .await @@ -522,7 +522,7 @@ pub async fn delete_user_role( user_from_db.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V2, ) .await @@ -537,7 +537,7 @@ pub async fn delete_user_role( user_from_db.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V1, ) .await @@ -584,7 +584,7 @@ pub async fn delete_user_role( user_from_db.get_user_id(), &user_from_token.org_id, &user_from_token.merchant_id, - user_from_token.profile_id.as_ref(), + &user_from_token.profile_id, UserRoleVersion::V1, ) .await @@ -676,17 +676,13 @@ pub async fn list_users_in_lineage( .await? } EntityType::Profile => { - let Some(profile_id) = user_from_token.profile_id.as_ref() else { - return Err(UserErrors::JwtProfileIdMissing.into()); - }; - utils::user_role::fetch_user_roles_by_payload( &state, ListUserRolesByOrgIdPayload { user_id: None, org_id: &user_from_token.org_id, merchant_id: Some(&user_from_token.merchant_id), - profile_id: Some(profile_id), + profile_id: Some(&user_from_token.profile_id), version: None, limit: None, }, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index d7d283819e5..93181c66142 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3030,7 +3030,7 @@ impl UserRoleInterface for KafkaStore { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { self.diesel_store @@ -3070,7 +3070,7 @@ impl UserRoleInterface for KafkaStore { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { self.diesel_store diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index 2d9a949879a..e4e564dc9a4 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -45,7 +45,7 @@ pub trait UserRoleInterface { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult; @@ -64,7 +64,7 @@ pub trait UserRoleInterface { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult; @@ -100,7 +100,7 @@ impl UserRoleInterface for Store { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; @@ -109,7 +109,7 @@ impl UserRoleInterface for Store { user_id.to_owned(), org_id.to_owned(), merchant_id.to_owned(), - profile_id.cloned(), + profile_id.to_owned(), version, ) .await @@ -146,7 +146,7 @@ impl UserRoleInterface for Store { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { let conn = connection::pg_connection_write(self).await?; @@ -155,7 +155,7 @@ impl UserRoleInterface for Store { user_id.to_owned(), org_id.to_owned(), merchant_id.to_owned(), - profile_id.cloned(), + profile_id.to_owned(), version, ) .await @@ -245,7 +245,7 @@ impl UserRoleInterface for MockDb { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { let user_roles = self.user_roles.lock().await; @@ -261,7 +261,7 @@ impl UserRoleInterface for MockDb { let profile_level_check = user_role.org_id.as_ref() == Some(org_id) && user_role.merchant_id.as_ref() == Some(merchant_id) - && user_role.profile_id.as_ref() == profile_id; + && user_role.profile_id.as_ref() == Some(profile_id); // Check if any condition matches and the version matches if user_role.user_id == user_id @@ -338,7 +338,7 @@ impl UserRoleInterface for MockDb { user_id: &str, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, - profile_id: Option<&id_type::ProfileId>, + profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { let mut user_roles = self.user_roles.lock().await; @@ -355,7 +355,7 @@ impl UserRoleInterface for MockDb { let profile_level_check = role.org_id.as_ref() == Some(org_id) && role.merchant_id.as_ref() == Some(merchant_id) - && role.profile_id.as_ref() == profile_id; + && role.profile_id.as_ref() == Some(profile_id); // Check if the user role matches the conditions and the version matches role.user_id == user_id diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 43f04487416..d4ac3bc846d 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -87,7 +87,7 @@ pub struct AuthenticationDataWithUser { pub merchant_account: domain::MerchantAccount, pub key_store: domain::MerchantKeyStore, pub user: storage::User, - pub profile_id: Option, + pub profile_id: id_type::ProfileId, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] @@ -231,7 +231,7 @@ pub struct AuthToken { pub role_id: String, pub exp: u64, pub org_id: id_type::OrganizationId, - pub profile_id: Option, + pub profile_id: id_type::ProfileId, pub tenant_id: Option, } @@ -243,7 +243,7 @@ impl AuthToken { role_id: String, settings: &Settings, org_id: id_type::OrganizationId, - profile_id: Option, + profile_id: id_type::ProfileId, tenant_id: Option, ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); @@ -267,7 +267,7 @@ pub struct UserFromToken { pub merchant_id: id_type::MerchantId, pub role_id: String, pub org_id: id_type::OrganizationId, - pub profile_id: Option, + pub profile_id: id_type::ProfileId, pub tenant_id: Option, } @@ -1829,7 +1829,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: payload.profile_id, + profile_id: Some(payload.profile_id), }; Ok(( @@ -2077,7 +2077,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: payload.profile_id, + profile_id: Some(payload.profile_id), }; Ok(( auth.clone(), @@ -2253,11 +2253,7 @@ where return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); } - if payload - .profile_id - .as_ref() - .is_some_and(|profile_id| *profile_id != self.profile_id) - { + if payload.profile_id != self.profile_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); } @@ -2290,7 +2286,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: payload.profile_id, + profile_id: Some(payload.profile_id), }; Ok(( auth.clone(), @@ -2354,30 +2350,14 @@ where .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) .attach_printable("Failed to fetch merchant account for the merchant id")?; - if let Some(ref payload_profile_id) = payload.profile_id { - if *payload_profile_id != self.profile_id { - return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); - } else { - // if both of them are same then proceed with the profile id present in the request - let auth = AuthenticationData { - merchant_account: merchant, - key_store, - profile_id: Some(self.profile_id.clone()), - }; - Ok(( - auth.clone(), - AuthenticationType::MerchantJwt { - merchant_id: auth.merchant_account.get_id().clone(), - user_id: Some(payload.user_id), - }, - )) - } + if payload.profile_id != self.profile_id { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); } else { - // if profile_id is not present in the auth_layer itself then no change in behaviour + // if both of them are same then proceed with the profile id present in the request let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: payload.profile_id, + profile_id: Some(self.profile_id.clone()), }; Ok(( auth.clone(), @@ -2527,7 +2507,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: payload.profile_id, + profile_id: Some(payload.profile_id), }; Ok(( auth, @@ -2663,7 +2643,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: payload.profile_id, + profile_id: Some(payload.profile_id), }; Ok(( (auth.clone(), payload.user_id.clone()), @@ -2781,7 +2761,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: payload.profile_id, + profile_id: Some(payload.profile_id), }; Ok(( auth.clone(), diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs index 86f10f1ceb7..634c781da7f 100644 --- a/crates/router/src/types/domain/user/decision_manager.rs +++ b/crates/router/src/types/domain/user/decision_manager.rs @@ -132,7 +132,7 @@ impl JWTFlow { .clone() .ok_or(report!(UserErrors::InternalServerError)) .attach_printable("org_id not found")?, - Some(profile_id), + profile_id, Some(user_role.tenant_id.clone()), ) .await diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c4647907ff9..8a9daefb287 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -100,7 +100,7 @@ pub async fn generate_jwt_auth_token_with_attributes( role_id, &state.conf, org_id, - Some(profile_id), + profile_id, tenant_id, ) .await?; From 0e026b70b6502c4e82f3e8cccc5441deb472119e Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:01:53 +0530 Subject: [PATCH 02/16] feat(payments): [Payment links] add hide card nickname field config for secure payment links (#6554) Co-authored-by: Chikke Srujan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 14 ++++++++- api-reference/openapi_spec.json | 14 ++++++++- crates/api_models/src/admin.rs | 5 ++++ crates/api_models/src/payments.rs | 2 ++ crates/common_utils/src/consts.rs | 25 ---------------- crates/diesel_models/src/business_profile.rs | 1 + crates/diesel_models/src/payment_intent.rs | 2 ++ crates/hyperswitch_domain_models/src/lib.rs | 3 ++ crates/router/src/consts.rs | 28 +++++++++++++++++ crates/router/src/core/payment_link.rs | 30 ++++++++++++++----- .../payment_link_initiator.js | 1 + .../secure_payment_link_initiator.js | 2 ++ .../router/src/core/payments/transformers.rs | 2 ++ crates/router/src/types/transformers.rs | 2 ++ 14 files changed, 96 insertions(+), 35 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index d2fa50d8629..f00998f154f 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -11948,7 +11948,8 @@ "seller_name", "sdk_layout", "display_sdk_only", - "enabled_saved_payment_method" + "enabled_saved_payment_method", + "hide_card_nickname_field" ], "properties": { "theme": { @@ -11975,6 +11976,10 @@ "type": "boolean", "description": "Enable saved payment method option for payment link" }, + "hide_card_nickname_field": { + "type": "boolean", + "description": "Hide card nickname field option for payment link" + }, "allowed_domains": { "type": "array", "items": { @@ -12039,6 +12044,13 @@ "example": true, "nullable": true }, + "hide_card_nickname_field": { + "type": "boolean", + "description": "Hide card nickname field option for payment link", + "default": false, + "example": true, + "nullable": true + }, "transaction_details": { "type": "array", "items": { diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 34d5be1b0ec..d4c5717d215 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -15074,7 +15074,8 @@ "seller_name", "sdk_layout", "display_sdk_only", - "enabled_saved_payment_method" + "enabled_saved_payment_method", + "hide_card_nickname_field" ], "properties": { "theme": { @@ -15101,6 +15102,10 @@ "type": "boolean", "description": "Enable saved payment method option for payment link" }, + "hide_card_nickname_field": { + "type": "boolean", + "description": "Hide card nickname field option for payment link" + }, "allowed_domains": { "type": "array", "items": { @@ -15165,6 +15170,13 @@ "example": true, "nullable": true }, + "hide_card_nickname_field": { + "type": "boolean", + "description": "Hide card nickname field option for payment link", + "default": false, + "example": true, + "nullable": true + }, "transaction_details": { "type": "array", "items": { diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 7f5ae1556d2..d80a4ea11b4 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2692,6 +2692,9 @@ pub struct PaymentLinkConfigRequest { /// Enable saved payment method option for payment link #[schema(default = false, example = true)] pub enabled_saved_payment_method: Option, + /// Hide card nickname field option for payment link + #[schema(default = false, example = true)] + pub hide_card_nickname_field: Option, /// Dynamic details related to merchant to be rendered in payment link pub transaction_details: Option>, } @@ -2735,6 +2738,8 @@ pub struct PaymentLinkConfig { pub display_sdk_only: bool, /// Enable saved payment method option for payment link pub enabled_saved_payment_method: bool, + /// Hide card nickname field option for payment link + pub hide_card_nickname_field: bool, /// A list of allowed domains (glob patterns) where this link can be embedded / opened from pub allowed_domains: Option>, /// Dynamic details related to merchant to be rendered in payment link diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 75dda21097a..37389308aa3 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6556,6 +6556,7 @@ pub struct PaymentLinkDetails { pub merchant_description: Option, pub sdk_layout: String, pub display_sdk_only: bool, + pub hide_card_nickname_field: bool, pub locale: Option, pub transaction_details: Option>, } @@ -6563,6 +6564,7 @@ pub struct PaymentLinkDetails { #[derive(Debug, serde::Serialize, Clone)] pub struct SecurePaymentLinkDetails { pub enabled_saved_payment_method: bool, + pub hide_card_nickname_field: bool, #[serde(flatten)] pub payment_link_details: PaymentLinkDetails, } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index a50555ced92..b43efcb1feb 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -1,7 +1,5 @@ //! Commonly used constants -use std::collections::HashSet; - /// Number of characters in a generated ID pub const ID_LENGTH: usize = 20; @@ -51,26 +49,12 @@ pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; /// Header Key for application overhead of a request pub const X_HS_LATENCY: &str = "x-hs-latency"; -/// Default Payment Link Background color -pub const DEFAULT_BACKGROUND_COLOR: &str = "#212E46"; - -/// Default product Img Link -pub const DEFAULT_PRODUCT_IMG: &str = - "https://live.hyperswitch.io/payment-link-assets/cart_placeholder.png"; - -/// Default Merchant Logo Link -pub const DEFAULT_MERCHANT_LOGO: &str = - "https://live.hyperswitch.io/payment-link-assets/Merchant_placeholder.png"; - /// Redirect url for Prophetpay pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/tokenize/"; /// Variable which store the card token for Prophetpay pub const PROPHETPAY_TOKEN: &str = "cctoken"; -/// Default SDK Layout -pub const DEFAULT_SDK_LAYOUT: &str = "tabs"; - /// Payment intent default client secret expiry (in seconds) pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; @@ -80,15 +64,6 @@ pub const DEFAULT_INTENT_FULFILLMENT_TIME: i64 = 15 * 60; /// Payment order fulfillment time (in seconds) pub const DEFAULT_ORDER_FULFILLMENT_TIME: i64 = 15 * 60; -/// Default bool for Display sdk only -pub const DEFAULT_DISPLAY_SDK_ONLY: bool = false; - -/// Default bool to enable saved payment method -pub const DEFAULT_ENABLE_SAVED_PAYMENT_METHOD: bool = false; - -/// Default allowed domains for payment links -pub const DEFAULT_ALLOWED_DOMAINS: Option> = None; - /// Default ttl for Extended card info in redis (in seconds) pub const DEFAULT_TTL_FOR_EXTENDED_CARD_INFO: u16 = 15 * 60; diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 05c124c4c9c..f4c7b86850e 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -542,6 +542,7 @@ pub struct PaymentLinkConfigRequest { pub sdk_layout: Option, pub display_sdk_only: Option, pub enabled_saved_payment_method: Option, + pub hide_card_nickname_field: Option, } common_utils::impl_to_sql_from_sql_json!(BusinessPaymentLinkConfig); diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 18248048110..26cb0b8c8a8 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -152,6 +152,8 @@ pub struct PaymentLinkConfigRequestForPayments { pub display_sdk_only: Option, /// Enable saved payment method option for payment link pub enabled_saved_payment_method: Option, + /// Hide card nickname field option for payment link + pub hide_card_nickname_field: Option, /// Dynamic details related to merchant to be rendered in payment link pub transaction_details: Option>, } diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 02795481ad9..386e0f01f38 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -190,6 +190,7 @@ impl ApiModelToDieselModelConvertor sdk_layout: item.sdk_layout, display_sdk_only: item.display_sdk_only, enabled_saved_payment_method: item.enabled_saved_payment_method, + hide_card_nickname_field: item.hide_card_nickname_field, transaction_details: item.transaction_details.map(|transaction_details| { transaction_details .into_iter() @@ -210,6 +211,7 @@ impl ApiModelToDieselModelConvertor sdk_layout, display_sdk_only, enabled_saved_payment_method, + hide_card_nickname_field, transaction_details, } = self; api_models::admin::PaymentLinkConfigRequest { @@ -219,6 +221,7 @@ impl ApiModelToDieselModelConvertor sdk_layout, display_sdk_only, enabled_saved_payment_method, + hide_card_nickname_field, transaction_details: transaction_details.map(|transaction_details| { transaction_details .into_iter() diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 3f4faedff81..51385593e9d 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -3,6 +3,8 @@ pub mod opensearch; pub mod user; pub mod user_role; +use std::collections::HashSet; + use common_utils::consts; pub use hyperswitch_interfaces::consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}; // ID generation @@ -145,6 +147,32 @@ pub const RECON_FEATURE_TAG: &str = "RECONCILIATION AND SETTLEMENT"; // Length of the unique reference ID generated for connector mandate requests pub const CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH: usize = 18; +/// Default allowed domains for payment links +pub const DEFAULT_ALLOWED_DOMAINS: Option> = None; + +/// Default hide card nickname field +pub const DEFAULT_HIDE_CARD_NICKNAME_FIELD: bool = false; + +/// Default bool for Display sdk only +pub const DEFAULT_DISPLAY_SDK_ONLY: bool = false; + +/// Default bool to enable saved payment method +pub const DEFAULT_ENABLE_SAVED_PAYMENT_METHOD: bool = false; + +/// Default Merchant Logo Link +pub const DEFAULT_MERCHANT_LOGO: &str = + "https://live.hyperswitch.io/payment-link-assets/Merchant_placeholder.png"; + +/// Default Payment Link Background color +pub const DEFAULT_BACKGROUND_COLOR: &str = "#212E46"; + +/// Default product Img Link +pub const DEFAULT_PRODUCT_IMG: &str = + "https://live.hyperswitch.io/payment-link-assets/cart_placeholder.png"; + +/// Default SDK Layout +pub const DEFAULT_SDK_LAYOUT: &str = "tabs"; + /// Vault Add request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub const ADD_VAULT_REQUEST_URL: &str = "/vault/add"; diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 09a2457197c..91829e56f2c 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -5,11 +5,7 @@ use api_models::{ payments::{PaymentLinkData, PaymentLinkStatusWrap}, }; use common_utils::{ - consts::{ - DEFAULT_ALLOWED_DOMAINS, DEFAULT_BACKGROUND_COLOR, DEFAULT_DISPLAY_SDK_ONLY, - DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, DEFAULT_LOCALE, DEFAULT_MERCHANT_LOGO, - DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY, - }, + consts::{DEFAULT_LOCALE, DEFAULT_SESSION_EXPIRY}, ext_traits::{AsyncExt, OptionExt, ValueExt}, types::{AmountConvertor, StringMajorUnitForCore}, }; @@ -25,7 +21,11 @@ use super::{ payments::helpers, }; use crate::{ - consts, + consts::{ + self, DEFAULT_ALLOWED_DOMAINS, DEFAULT_BACKGROUND_COLOR, DEFAULT_DISPLAY_SDK_ONLY, + DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, DEFAULT_HIDE_CARD_NICKNAME_FIELD, + DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, + }, errors::RouterResponse, get_payment_link_config_value, get_payment_link_config_value_based_on_priority, headers::ACCEPT_LANGUAGE, @@ -125,6 +125,7 @@ pub async fn form_payment_link_data( sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(), display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY, enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, + hide_card_nickname_field: DEFAULT_HIDE_CARD_NICKNAME_FIELD, allowed_domains: DEFAULT_ALLOWED_DOMAINS, transaction_details: None, } @@ -265,6 +266,7 @@ pub async fn form_payment_link_data( merchant_description: payment_intent.description, sdk_layout: payment_link_config.sdk_layout.clone(), display_sdk_only: payment_link_config.display_sdk_only, + hide_card_nickname_field: payment_link_config.hide_card_nickname_field, locale, transaction_details: payment_link_config.transaction_details.clone(), }; @@ -322,6 +324,7 @@ pub async fn initiate_secure_payment_link_flow( PaymentLinkData::PaymentLinkDetails(link_details) => { let secure_payment_link_details = api_models::payments::SecurePaymentLinkDetails { enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method, + hide_card_nickname_field: payment_link_config.hide_card_nickname_field, payment_link_details: *link_details.to_owned(), }; let js_script = format!( @@ -607,7 +610,15 @@ pub fn get_payment_link_config_based_on_priority( (default_domain_name, None, None) }; - let (theme, logo, seller_name, sdk_layout, display_sdk_only, enabled_saved_payment_method) = get_payment_link_config_value!( + let ( + theme, + logo, + seller_name, + sdk_layout, + display_sdk_only, + enabled_saved_payment_method, + hide_card_nickname_field, + ) = get_payment_link_config_value!( payment_create_link_config, business_theme_configs, (theme, DEFAULT_BACKGROUND_COLOR.to_string()), @@ -618,7 +629,8 @@ pub fn get_payment_link_config_based_on_priority( ( enabled_saved_payment_method, DEFAULT_ENABLE_SAVED_PAYMENT_METHOD - ) + ), + (hide_card_nickname_field, DEFAULT_HIDE_CARD_NICKNAME_FIELD) ); let payment_link_config = PaymentLinkConfig { theme, @@ -627,6 +639,7 @@ pub fn get_payment_link_config_based_on_priority( sdk_layout, display_sdk_only, enabled_saved_payment_method, + hide_card_nickname_field, allowed_domains, transaction_details: payment_create_link_config .and_then(|payment_link_config| payment_link_config.theme_config.transaction_details), @@ -729,6 +742,7 @@ pub async fn get_payment_link_status( sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(), display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY, enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, + hide_card_nickname_field: DEFAULT_HIDE_CARD_NICKNAME_FIELD, allowed_domains: DEFAULT_ALLOWED_DOMAINS, transaction_details: None, } diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js index 8d1183a2535..1264915592d 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js @@ -56,6 +56,7 @@ function initializeSDK() { height: 55, }, }, + hideCardNicknameField: false, }; // @ts-ignore unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); diff --git a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js index c901148bbfa..5080970ce3c 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js @@ -63,6 +63,7 @@ if (!isFramed) { : paymentDetails.sdk_layout; var enableSavedPaymentMethod = paymentDetails.enabled_saved_payment_method; + var hideCardNicknameField = paymentDetails.hide_card_nickname_field; var unifiedCheckoutOptions = { displaySavedPaymentMethodsCheckbox: enableSavedPaymentMethod, displaySavedPaymentMethods: enableSavedPaymentMethod, @@ -79,6 +80,7 @@ if (!isFramed) { height: 55, }, }, + hideCardNicknameField: hideCardNicknameField, }; // @ts-ignore unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index b09034e28cb..8a0983eccab 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -3518,6 +3518,7 @@ impl ForeignFrom sdk_layout: config.sdk_layout, display_sdk_only: config.display_sdk_only, enabled_saved_payment_method: config.enabled_saved_payment_method, + hide_card_nickname_field: config.hide_card_nickname_field, transaction_details: config.transaction_details.map(|transaction_details| { transaction_details .iter() @@ -3570,6 +3571,7 @@ impl ForeignFrom sdk_layout: config.sdk_layout, display_sdk_only: config.display_sdk_only, enabled_saved_payment_method: config.enabled_saved_payment_method, + hide_card_nickname_field: config.hide_card_nickname_field, transaction_details: config.transaction_details.map(|transaction_details| { transaction_details .iter() diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index c97084a2297..36865f5ce09 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1987,6 +1987,7 @@ impl ForeignFrom sdk_layout: item.sdk_layout, display_sdk_only: item.display_sdk_only, enabled_saved_payment_method: item.enabled_saved_payment_method, + hide_card_nickname_field: item.hide_card_nickname_field, } } } @@ -2002,6 +2003,7 @@ impl ForeignFrom sdk_layout: item.sdk_layout, display_sdk_only: item.display_sdk_only, enabled_saved_payment_method: item.enabled_saved_payment_method, + hide_card_nickname_field: item.hide_card_nickname_field, transaction_details: None, } } From 053f8109302a98e6b6d30d957b2af618ea73055f Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Mon, 18 Nov 2024 18:53:50 +0530 Subject: [PATCH 03/16] refactor(core): add profile_id for default_fallback api (#6546) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/core/routing.rs | 26 ++++++++++++++----------- crates/router/src/routes/app.rs | 30 +++++++++-------------------- crates/router/src/routes/routing.rs | 16 ++++++--------- 3 files changed, 30 insertions(+), 42 deletions(-) diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 0bd38918ee7..94eb9bd741e 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -909,26 +909,30 @@ pub async fn retrieve_default_fallback_algorithm_for_profile( } #[cfg(feature = "v1")] - pub async fn retrieve_default_routing_config( state: SessionState, + profile_id: Option, merchant_account: domain::MerchantAccount, transaction_type: &enums::TransactionType, ) -> RouterResponse> { metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); + let id = profile_id + .map(|profile_id| profile_id.get_string_repr().to_owned()) + .unwrap_or_else(|| merchant_account.get_id().get_string_repr().to_string()); - helpers::get_merchant_default_config( - db, - merchant_account.get_id().get_string_repr(), - transaction_type, - ) - .await - .map(|conn_choice| { - metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); - service_api::ApplicationResponse::Json(conn_choice) - }) + helpers::get_merchant_default_config(db, &id, transaction_type) + .await + .map(|conn_choice| { + metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); + service_api::ApplicationResponse::Json(conn_choice) + }) } + #[cfg(feature = "v2")] pub async fn retrieve_routing_config_under_profile( state: SessionState, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ebf6c7d384d..892dfa62a36 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -758,22 +758,14 @@ impl Routing { }, ))) .service( - web::resource("/default") - .route(web::get().to(|state, req| { - routing::routing_retrieve_default_config( - state, - req, - &TransactionType::Payment, - ) - })) - .route(web::post().to(|state, req, payload| { - routing::routing_update_default_config( - state, - req, - payload, - &TransactionType::Payment, - ) - })), + web::resource("/default").route(web::post().to(|state, req, payload| { + routing::routing_update_default_config( + state, + req, + payload, + &TransactionType::Payment, + ) + })), ) .service( web::resource("/deactivate").route(web::post().to(|state, req, payload| { @@ -807,11 +799,7 @@ impl Routing { ) .service( web::resource("/default/profile").route(web::get().to(|state, req| { - routing::routing_retrieve_default_config_for_profiles( - state, - req, - &TransactionType::Payment, - ) + routing::routing_retrieve_default_config(state, req, &TransactionType::Payment) })), ); diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index f0269bd029a..b861b54b5b5 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -565,17 +565,13 @@ pub async fn routing_retrieve_default_config( &req, (), |state, auth: auth::AuthenticationData, _, _| { - routing::retrieve_default_routing_config(state, auth.merchant_account, transaction_type) + routing::retrieve_default_routing_config( + state, + auth.profile_id, + auth.merchant_account, + transaction_type, + ) }, - #[cfg(not(feature = "release"))] - auth::auth_type( - &auth::HeaderAuth(auth::ApiKeyAuth), - &auth::JWTAuth { - permission: Permission::ProfileRoutingRead, - }, - req.headers(), - ), - #[cfg(feature = "release")] &auth::JWTAuth { permission: Permission::ProfileRoutingRead, }, From 6881ce2ed3d11006c33fef9863107f0d823ebddb Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:18:51 +0530 Subject: [PATCH 04/16] feat(connector): [Novalnet] Add support for disputes (#6560) --- .../src/connectors/novalnet.rs | 42 ++++++++++++++++++- .../src/connectors/novalnet/transformers.rs | 23 ++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet.rs b/crates/hyperswitch_connectors/src/connectors/novalnet.rs index 7450a48884a..8331e229acb 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet.rs @@ -37,7 +37,7 @@ use hyperswitch_interfaces::{ ConnectorValidation, }, configs::Connectors, - errors, + disputes, errors, events::connector_api_logs::ConnectorEvent, types::{self, Response}, webhooks, @@ -877,4 +877,44 @@ impl webhooks::IncomingWebhook for Novalnet { .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; Ok(Box::new(notif)) } + + fn get_dispute_details( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let notif: transformers::NovalnetWebhookNotificationResponse = + get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let (amount, currency, reason, reason_code) = match notif.transaction { + novalnet::NovalnetWebhookTransactionData::CaptureTransactionData(data) => { + (data.amount, data.currency, None, None) + } + novalnet::NovalnetWebhookTransactionData::CancelTransactionData(data) => { + (data.amount, data.currency, None, None) + } + + novalnet::NovalnetWebhookTransactionData::RefundsTransactionData(data) => { + (data.amount, data.currency, None, None) + } + + novalnet::NovalnetWebhookTransactionData::SyncTransactionData(data) => { + (data.amount, data.currency, data.reason, data.reason_code) + } + }; + + let dispute_status = + novalnet::get_novalnet_dispute_status(notif.event.event_type).to_string(); + Ok(disputes::DisputePayload { + amount: novalnet::option_to_result(amount)?.to_string(), + currency: novalnet::option_to_result(currency)?.to_string(), + dispute_stage: api_models::enums::DisputeStage::Dispute, + connector_dispute_id: notif.event.tid.to_string(), + connector_reason: reason, + connector_reason_code: reason_code, + challenge_required_by: None, + connector_status: dispute_status, + created_at: None, + updated_at: None, + }) + } } diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index 313e35eeb0e..1888511e9ba 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -1294,6 +1294,8 @@ pub enum WebhookEventType { TransactionCapture, TransactionCancel, TransactionRefund, + Chargeback, + Credit, } #[derive(Serialize, Deserialize, Debug)] @@ -1356,9 +1358,30 @@ pub fn get_incoming_webhook_event( } _ => IncomingWebhookEvent::RefundFailure, }, + WebhookEventType::Chargeback => IncomingWebhookEvent::DisputeOpened, + WebhookEventType::Credit => IncomingWebhookEvent::DisputeWon, } } pub fn reverse_string(s: &str) -> String { s.chars().rev().collect() } + +#[derive(Display, Debug, Serialize, Deserialize)] +pub enum WebhookDisputeStatus { + DisputeOpened, + DisputeWon, + Unknown, +} + +pub fn get_novalnet_dispute_status(status: WebhookEventType) -> WebhookDisputeStatus { + match status { + WebhookEventType::Chargeback => WebhookDisputeStatus::DisputeOpened, + WebhookEventType::Credit => WebhookDisputeStatus::DisputeWon, + _ => WebhookDisputeStatus::Unknown, + } +} + +pub fn option_to_result(opt: Option) -> Result { + opt.ok_or(errors::ConnectorError::WebhookBodyDecodingFailed) +} From 65bf75a75e1e7d705de3ee3e9080a7a86099ffc3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 00:21:48 +0000 Subject: [PATCH 05/16] chore(version): 2024.11.19.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7daa3cd109b..640a259efa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.19.0 + +### Features + +- **connector:** [Novalnet] Add support for disputes ([#6560](https://github.com/juspay/hyperswitch/pull/6560)) ([`6881ce2`](https://github.com/juspay/hyperswitch/commit/6881ce2ed3d11006c33fef9863107f0d823ebddb)) +- **payments:** [Payment links] add hide card nickname field config for secure payment links ([#6554](https://github.com/juspay/hyperswitch/pull/6554)) ([`0e026b7`](https://github.com/juspay/hyperswitch/commit/0e026b70b6502c4e82f3e8cccc5441deb472119e)) + +### Refactors + +- **core:** Add profile_id for default_fallback api ([#6546](https://github.com/juspay/hyperswitch/pull/6546)) ([`053f810`](https://github.com/juspay/hyperswitch/commit/053f8109302a98e6b6d30d957b2af618ea73055f)) +- **users:** Make `profile_id` in the JWT non-optional ([#6537](https://github.com/juspay/hyperswitch/pull/6537)) ([`d32397f`](https://github.com/juspay/hyperswitch/commit/d32397f060731f51a15634e221117a554b8b3721)) + +**Full Changelog:** [`2024.11.18.0...2024.11.19.0`](https://github.com/juspay/hyperswitch/compare/2024.11.18.0...2024.11.19.0) + +- - - + ## 2024.11.18.0 ### Features From 8e9c3ec8931851dae638037b91eb1611399be0bf Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:13:58 +0530 Subject: [PATCH 06/16] feat(router): add payment incoming webhooks support for v2 (#6551) Co-authored-by: Narayan Bhat Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: hrithikesh026 --- crates/api_models/src/payments.rs | 99 ++- crates/api_models/src/webhooks.rs | 51 ++ crates/common_utils/src/events.rs | 6 + .../src/id_type/global_id/payment.rs | 13 + crates/diesel_models/src/payment_attempt.rs | 2 +- .../src/query/payment_attempt.rs | 27 + .../hyperswitch_domain_models/src/payments.rs | 10 + .../src/payments/payment_attempt.rs | 16 + .../src/router_data.rs | 1 + crates/router/src/core.rs | 1 - crates/router/src/core/payments.rs | 5 + crates/router/src/core/webhooks.rs | 7 + .../router/src/core/webhooks/incoming_v2.rs | 753 ++++++++++++++++++ .../src/core/webhooks/webhook_events.rs | 1 + crates/router/src/db/kafka_store.rs | 20 + .../src/events/outgoing_webhook_logs.rs | 46 ++ crates/router/src/lib.rs | 3 +- crates/router/src/routes.rs | 1 - crates/router/src/routes/app.rs | 25 +- crates/router/src/routes/webhook_events.rs | 1 + crates/router/src/routes/webhooks.rs | 43 + crates/router/src/services/authentication.rs | 59 ++ crates/router/src/types/api/payments.rs | 22 + .../src/mock_db/payment_attempt.rs | 13 + .../src/payments/payment_attempt.rs | 55 +- 25 files changed, 1271 insertions(+), 9 deletions(-) create mode 100644 crates/router/src/core/webhooks/incoming_v2.rs diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 37389308aa3..2f7f796aef1 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3691,6 +3691,7 @@ pub struct PaymentMethodDataResponseWithBilling { } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] +#[cfg(feature = "v1")] pub enum PaymentIdType { /// The identifier for payment intent PaymentIntentId(id_type::PaymentId), @@ -3702,6 +3703,20 @@ pub enum PaymentIdType { PreprocessingId(String), } +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] +#[cfg(feature = "v2")] +pub enum PaymentIdType { + /// The identifier for payment intent + PaymentIntentId(id_type::GlobalPaymentId), + /// The identifier for connector transaction + ConnectorTransactionId(String), + /// The identifier for payment attempt + PaymentAttemptId(String), + /// The identifier for preprocessing step + PreprocessingId(String), +} + +#[cfg(feature = "v1")] impl fmt::Display for PaymentIdType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -3726,6 +3741,7 @@ impl fmt::Display for PaymentIdType { } } +#[cfg(feature = "v1")] impl Default for PaymentIdType { fn default() -> Self { Self::PaymentIntentId(Default::default()) @@ -4566,7 +4582,7 @@ pub struct PaymentsRetrieveRequest { /// Error details for the payment #[cfg(feature = "v2")] -#[derive(Debug, serde::Serialize, ToSchema)] +#[derive(Debug, serde::Serialize, Clone, ToSchema)] pub struct ErrorDetails { /// The error code pub code: String, @@ -4651,7 +4667,7 @@ pub struct PaymentsConfirmIntentResponse { // TODO: have a separate response for detailed, summarized /// Response for Payment Intent Confirm #[cfg(feature = "v2")] -#[derive(Debug, serde::Serialize, ToSchema)] +#[derive(Debug, serde::Serialize, Clone, ToSchema)] pub struct PaymentsRetrieveResponse { /// Unique identifier for the payment. This ensures idempotency for multiple payments /// that have been done by a single merchant. @@ -6307,6 +6323,85 @@ pub struct FrmMessage { pub frm_error: Option, } +#[cfg(feature = "v2")] +mod payment_id_type { + use std::{borrow::Cow, fmt}; + + use serde::{ + de::{self, Visitor}, + Deserializer, + }; + + use super::PaymentIdType; + + struct PaymentIdVisitor; + struct OptionalPaymentIdVisitor; + + impl<'de> Visitor<'de> for PaymentIdVisitor { + type Value = PaymentIdType; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("payment id") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + common_utils::id_type::GlobalPaymentId::try_from(Cow::Owned(value.to_string())) + .map_err(de::Error::custom) + .map(PaymentIdType::PaymentIntentId) + } + } + + impl<'de> Visitor<'de> for OptionalPaymentIdVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("payment id") + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(PaymentIdVisitor).map(Some) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } + } + + #[allow(dead_code)] + pub(crate) fn deserialize<'a, D>(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + deserializer.deserialize_any(PaymentIdVisitor) + } + + pub(crate) fn deserialize_option<'a, D>( + deserializer: D, + ) -> Result, D::Error> + where + D: Deserializer<'a>, + { + deserializer.deserialize_option(OptionalPaymentIdVisitor) + } +} + +#[cfg(feature = "v1")] mod payment_id_type { use std::{borrow::Cow, fmt}; diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index 56a566d170a..e6f4065eb7a 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -76,25 +76,45 @@ pub enum WebhookFlow { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] /// This enum tells about the affect a webhook had on an object pub enum WebhookResponseTracker { + #[cfg(feature = "v1")] Payment { payment_id: common_utils::id_type::PaymentId, status: common_enums::IntentStatus, }, + #[cfg(feature = "v2")] + Payment { + payment_id: common_utils::id_type::GlobalPaymentId, + status: common_enums::IntentStatus, + }, #[cfg(feature = "payouts")] Payout { payout_id: String, status: common_enums::PayoutStatus, }, + #[cfg(feature = "v1")] Refund { payment_id: common_utils::id_type::PaymentId, refund_id: String, status: common_enums::RefundStatus, }, + #[cfg(feature = "v2")] + Refund { + payment_id: common_utils::id_type::GlobalPaymentId, + refund_id: String, + status: common_enums::RefundStatus, + }, + #[cfg(feature = "v1")] Dispute { dispute_id: String, payment_id: common_utils::id_type::PaymentId, status: common_enums::DisputeStatus, }, + #[cfg(feature = "v2")] + Dispute { + dispute_id: String, + payment_id: common_utils::id_type::GlobalPaymentId, + status: common_enums::DisputeStatus, + }, Mandate { mandate_id: String, status: common_enums::MandateStatus, @@ -103,6 +123,7 @@ pub enum WebhookResponseTracker { } impl WebhookResponseTracker { + #[cfg(feature = "v1")] pub fn get_payment_id(&self) -> Option { match self { Self::Payment { payment_id, .. } @@ -113,6 +134,18 @@ impl WebhookResponseTracker { Self::Payout { .. } => None, } } + + #[cfg(feature = "v2")] + pub fn get_payment_id(&self) -> Option { + match self { + Self::Payment { payment_id, .. } + | Self::Refund { payment_id, .. } + | Self::Dispute { payment_id, .. } => Some(payment_id.to_owned()), + Self::NoEffect | Self::Mandate { .. } => None, + #[cfg(feature = "payouts")] + Self::Payout { .. } => None, + } + } } impl From for WebhookFlow { @@ -227,6 +260,7 @@ pub struct OutgoingWebhook { #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(tag = "type", content = "object", rename_all = "snake_case")] +#[cfg(feature = "v1")] pub enum OutgoingWebhookContent { #[schema(value_type = PaymentsResponse, title = "PaymentsResponse")] PaymentDetails(Box), @@ -241,6 +275,23 @@ pub enum OutgoingWebhookContent { PayoutDetails(Box), } +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(tag = "type", content = "object", rename_all = "snake_case")] +#[cfg(feature = "v2")] +pub enum OutgoingWebhookContent { + #[schema(value_type = PaymentsResponse, title = "PaymentsResponse")] + PaymentDetails(Box), + #[schema(value_type = RefundResponse, title = "RefundResponse")] + RefundDetails(Box), + #[schema(value_type = DisputeResponse, title = "DisputeResponse")] + DisputeDetails(Box), + #[schema(value_type = MandateResponse, title = "MandateResponse")] + MandateDetails(Box), + #[cfg(feature = "payouts")] + #[schema(value_type = PayoutCreateResponse, title = "PayoutCreateResponse")] + PayoutDetails(Box), +} + #[derive(Debug, Clone, Serialize)] pub struct ConnectorWebhookSecrets { pub secret: Vec, diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index afe19d41239..3b86ea8e88b 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -60,10 +60,16 @@ pub enum ApiEventsType { PaymentMethodList { payment_id: Option, }, + #[cfg(feature = "v1")] Webhooks { connector: String, payment_id: Option, }, + #[cfg(feature = "v2")] + Webhooks { + connector: id_type::MerchantConnectorAccountId, + payment_id: Option, + }, Routing, ResourceListAPI, #[cfg(feature = "v1")] diff --git a/crates/common_utils/src/id_type/global_id/payment.rs b/crates/common_utils/src/id_type/global_id/payment.rs index 6e5848e5a38..934d710604c 100644 --- a/crates/common_utils/src/id_type/global_id/payment.rs +++ b/crates/common_utils/src/id_type/global_id/payment.rs @@ -66,3 +66,16 @@ impl GlobalAttemptId { self.0.get_string_repr() } } + +impl TryFrom> for GlobalAttemptId { + type Error = error_stack::Report; + fn try_from(value: std::borrow::Cow<'static, str>) -> Result { + use error_stack::ResultExt; + let global_attempt_id = super::GlobalId::from_string(value).change_context( + errors::ValidationError::IncorrectValueProvided { + field_name: "payment_id", + }, + )?; + Ok(Self(global_attempt_id)) + } +} diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 527d0e7c027..7760ea76c50 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -765,7 +765,7 @@ pub struct PaymentAttemptUpdateInternal { pub browser_info: Option, // payment_token: Option, pub error_code: Option, - // connector_metadata: Option, + pub connector_metadata: Option, // payment_method_data: Option, // payment_experience: Option, // preprocessing_step_id: Option, diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index b52e3e2d330..46a84488898 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -188,6 +188,33 @@ impl PaymentAttempt { .await } + #[cfg(feature = "v2")] + pub async fn find_by_profile_id_connector_transaction_id( + conn: &PgPooledConn, + profile_id: &common_utils::id_type::ProfileId, + connector_txn_id: &str, + ) -> StorageResult { + let (txn_id, txn_data) = common_utils::types::ConnectorTransactionId::form_id_and_data( + connector_txn_id.to_string(), + ); + let connector_transaction_id = txn_id + .get_txn_id(txn_data.as_ref()) + .change_context(DatabaseError::Others) + .attach_printable_lazy(|| { + format!( + "Failed to retrieve txn_id for ({:?}, {:?})", + txn_id, txn_data + ) + })?; + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::profile_id + .eq(profile_id.to_owned()) + .and(dsl::connector_payment_id.eq(connector_transaction_id.to_owned())), + ) + .await + } + #[cfg(feature = "v1")] pub async fn find_by_merchant_id_attempt_id( conn: &PgPooledConn, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 5c629f6198e..1bab8ae3b76 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -574,3 +574,13 @@ where /// This will depend on the payment status and the force sync flag in the request pub should_sync_with_connector: bool, } + +#[cfg(feature = "v2")] +impl PaymentStatusData +where + F: Clone, +{ + pub fn get_payment_id(&self) -> &id_type::GlobalPaymentId { + &self.payment_intent.id + } +} diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index db3ae20177c..ec1463d1b7b 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -102,6 +102,16 @@ pub trait PaymentAttemptInterface { storage_scheme: storage_enums::MerchantStorageScheme, ) -> error_stack::Result; + #[cfg(feature = "v2")] + async fn find_payment_attempt_by_profile_id_connector_transaction_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_id: &id_type::ProfileId, + connector_transaction_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult; + #[cfg(feature = "v1")] async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id( &self, @@ -1309,6 +1319,7 @@ pub enum PaymentAttemptUpdate { connector_payment_id: Option, updated_by: String, redirection_data: Option, + connector_metadata: Option, }, /// Update the payment attempt after force syncing with the connector SyncUpdate { @@ -1943,6 +1954,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_payment_id: None, connector: Some(connector), redirection_data: None, + connector_metadata: None, }, PaymentAttemptUpdate::ErrorUpdate { status, @@ -1963,12 +1975,14 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_payment_id, connector: None, redirection_data: None, + connector_metadata: None, }, PaymentAttemptUpdate::ConfirmIntentResponse { status, connector_payment_id, updated_by, redirection_data, + connector_metadata, } => Self { status: Some(status), error_message: None, @@ -1984,6 +1998,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: redirection_data .map(diesel_models::payment_attempt::RedirectForm::from), + connector_metadata, }, PaymentAttemptUpdate::SyncUpdate { status, updated_by } => Self { status: Some(status), @@ -1999,6 +2014,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_payment_id: None, connector: None, redirection_data: None, + connector_metadata: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index f995c9a1e57..0863ab59b45 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -471,6 +471,7 @@ impl connector_payment_id, updated_by: storage_scheme.to_string(), redirection_data: *redirection_data.clone(), + connector_metadata: connector_metadata.clone().map(Secret::new), } } router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index e2ccca46298..a2da3384196 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -51,5 +51,4 @@ pub mod utils; pub mod verification; #[cfg(feature = "olap")] pub mod verify_connector; -#[cfg(feature = "v1")] pub mod webhooks; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index ed41650840a..dd481467f16 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1642,11 +1642,16 @@ pub trait PaymentRedirectFlow: Sync { let query_params = req.param.clone().get_required_value("param")?; + #[cfg(feature = "v1")] let resource_id = api::PaymentIdTypeExt::get_payment_intent_id(&req.resource_id) .change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_id", })?; + #[cfg(feature = "v2")] + //TODO: Will get the global payment id from the resource id, we need to handle this in the further flow + let resource_id: id_type::PaymentId = todo!(); + // This connector data is ephemeral, the call payment flow will get new connector data // with merchant account details, so the connector_id can be safely set to None here let connector_data = api::ConnectorData::get_connector_by_name( diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index a209d90f316..64c5617398b 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -1,10 +1,17 @@ +#[cfg(feature = "v1")] mod incoming; +#[cfg(feature = "v2")] +mod incoming_v2; +#[cfg(feature = "v1")] mod outgoing; pub mod types; pub mod utils; #[cfg(feature = "olap")] pub mod webhook_events; +#[cfg(feature = "v2")] +pub(crate) use self::incoming_v2::incoming_webhooks_wrapper; +#[cfg(feature = "v1")] pub(crate) use self::{ incoming::incoming_webhooks_wrapper, outgoing::{ diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs new file mode 100644 index 00000000000..0deb91efaa8 --- /dev/null +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -0,0 +1,753 @@ +use std::{marker::PhantomData, str::FromStr, time::Instant}; + +use actix_web::FromRequest; +use api_models::webhooks::{self, WebhookResponseTracker}; +use common_utils::{ + errors::ReportSwitchExt, events::ApiEventsType, types::keymanager::KeyManagerState, +}; +use error_stack::ResultExt; +use hyperswitch_domain_models::{ + payments::{HeaderPayload, PaymentStatusData}, + router_request_types::VerifyWebhookSourceRequestData, + router_response_types::{VerifyWebhookSourceResponseData, VerifyWebhookStatus}, +}; +use hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails; +use router_env::{instrument, metrics::add_attributes, tracing, tracing_actix_web::RequestId}; + +use super::{types, utils, MERCHANT_ID}; +use crate::{ + core::{ + api_locking, + errors::{self, ConnectorErrorExt, CustomResult, RouterResponse, StorageErrorExt}, + metrics, + payments::{self, transformers::ToResponse}, + webhooks::utils::construct_webhook_router_data, + }, + db::StorageInterface, + events::api_logs::ApiEvent, + logger, + routes::{ + app::{ReqState, SessionStateInfo}, + lock_utils, SessionState, + }, + services::{ + self, authentication as auth, connector_integration_interface::ConnectorEnum, + ConnectorValidation, + }, + types::{ + api::{self, ConnectorData, GetToken, IncomingWebhook}, + domain, + storage::enums, + transformers::ForeignInto, + }, +}; + +#[allow(clippy::too_many_arguments)] +pub async fn incoming_webhooks_wrapper( + flow: &impl router_env::types::FlowMetric, + state: SessionState, + req_state: ReqState, + req: &actix_web::HttpRequest, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + connector_id: &common_utils::id_type::MerchantConnectorAccountId, + body: actix_web::web::Bytes, +) -> RouterResponse { + let start_instant = Instant::now(); + let (application_response, webhooks_response_tracker, serialized_req) = + Box::pin(incoming_webhooks_core::( + state.clone(), + req_state, + req, + merchant_account.clone(), + profile, + key_store, + connector_id, + body.clone(), + )) + .await?; + + logger::info!(incoming_webhook_payload = ?serialized_req); + + let request_duration = Instant::now() + .saturating_duration_since(start_instant) + .as_millis(); + + let request_id = RequestId::extract(req) + .await + .attach_printable("Unable to extract request id from request") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let auth_type = auth::AuthenticationType::WebhookAuth { + merchant_id: merchant_account.get_id().clone(), + }; + let status_code = 200; + let api_event = ApiEventsType::Webhooks { + connector: connector_id.clone(), + payment_id: webhooks_response_tracker.get_payment_id(), + }; + let response_value = serde_json::to_value(&webhooks_response_tracker) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not convert webhook effect to string")?; + + let api_event = ApiEvent::new( + Some(merchant_account.get_id().clone()), + flow, + &request_id, + request_duration, + status_code, + serialized_req, + Some(response_value), + None, + auth_type, + None, + api_event, + req, + req.method(), + ); + state.event_handler().log_event(&api_event); + Ok(application_response) +} + +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +async fn incoming_webhooks_core( + state: SessionState, + req_state: ReqState, + req: &actix_web::HttpRequest, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + connector_id: &common_utils::id_type::MerchantConnectorAccountId, + body: actix_web::web::Bytes, +) -> errors::RouterResult<( + services::ApplicationResponse, + WebhookResponseTracker, + serde_json::Value, +)> { + metrics::WEBHOOK_INCOMING_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::KeyValue::new( + MERCHANT_ID, + merchant_account.get_id().get_string_repr().to_owned(), + )], + ); + let mut request_details = IncomingWebhookRequestDetails { + method: req.method().clone(), + uri: req.uri().clone(), + headers: req.headers(), + query_params: req.query_string().to_string(), + body: &body, + }; + + // Fetch the merchant connector account to get the webhooks source secret + // `webhooks source secret` is a secret shared between the merchant and connector + // This is used for source verification and webhooks integrity + let (merchant_connector_account, connector, connector_name) = + fetch_mca_and_connector(&state, connector_id, &key_store).await?; + + let decoded_body = connector + .decode_webhook_body( + &request_details, + merchant_account.get_id(), + merchant_connector_account.connector_webhook_details.clone(), + connector_name.as_str(), + ) + .await + .switch() + .attach_printable("There was an error in incoming webhook body decoding")?; + + request_details.body = &decoded_body; + + let event_type = match connector + .get_webhook_event_type(&request_details) + .allow_webhook_event_type_not_found( + state + .clone() + .conf + .webhooks + .ignore_error + .event_type + .unwrap_or(true), + ) + .switch() + .attach_printable("Could not find event type in incoming webhook body")? + { + Some(event_type) => event_type, + // Early return allows us to acknowledge the webhooks that we do not support + None => { + logger::error!( + webhook_payload =? request_details.body, + "Failed while identifying the event type", + ); + + metrics::WEBHOOK_EVENT_TYPE_IDENTIFICATION_FAILURE_COUNT.add( + &metrics::CONTEXT, + 1, + &[ + metrics::KeyValue::new(MERCHANT_ID, merchant_account.get_id().clone()), + metrics::KeyValue::new("connector", connector_name.to_string()), + ], + ); + + let response = connector + .get_webhook_api_response(&request_details) + .switch() + .attach_printable("Failed while early return in case of event type parsing")?; + + return Ok(( + response, + WebhookResponseTracker::NoEffect, + serde_json::Value::Null, + )); + } + }; + logger::info!(event_type=?event_type); + + let is_webhook_event_supported = !matches!( + event_type, + webhooks::IncomingWebhookEvent::EventNotSupported + ); + let is_webhook_event_enabled = !utils::is_webhook_event_disabled( + &*state.clone().store, + connector_name.as_str(), + merchant_account.get_id(), + &event_type, + ) + .await; + + //process webhook further only if webhook event is enabled and is not event_not_supported + let process_webhook_further = is_webhook_event_enabled && is_webhook_event_supported; + + logger::info!(process_webhook=?process_webhook_further); + + let flow_type: api::WebhookFlow = event_type.into(); + let mut event_object: Box = Box::new(serde_json::Value::Null); + let webhook_effect = if process_webhook_further + && !matches!(flow_type, api::WebhookFlow::ReturnResponse) + { + let object_ref_id = connector + .get_webhook_object_reference_id(&request_details) + .switch() + .attach_printable("Could not find object reference id in incoming webhook body")?; + let connector_enum = api_models::enums::Connector::from_str(&connector_name) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| { + format!("unable to parse connector name {connector_name:?}") + })?; + let connectors_with_source_verification_call = &state.conf.webhook_source_verification_call; + + let source_verified = if connectors_with_source_verification_call + .connectors_with_webhook_source_verification_call + .contains(&connector_enum) + { + verify_webhook_source_verification_call( + connector.clone(), + &state, + &merchant_account, + merchant_connector_account.clone(), + &connector_name, + &request_details, + ) + .await + .or_else(|error| match error.current_context() { + errors::ConnectorError::WebhookSourceVerificationFailed => { + logger::error!(?error, "Source Verification Failed"); + Ok(false) + } + _ => Err(error), + }) + .switch() + .attach_printable("There was an issue in incoming webhook source verification")? + } else { + connector + .clone() + .verify_webhook_source( + &request_details, + merchant_account.get_id(), + merchant_connector_account.connector_webhook_details.clone(), + merchant_connector_account.connector_account_details.clone(), + connector_name.as_str(), + ) + .await + .or_else(|error| match error.current_context() { + errors::ConnectorError::WebhookSourceVerificationFailed => { + logger::error!(?error, "Source Verification Failed"); + Ok(false) + } + _ => Err(error), + }) + .switch() + .attach_printable("There was an issue in incoming webhook source verification")? + }; + + logger::info!(source_verified=?source_verified); + + if source_verified { + metrics::WEBHOOK_SOURCE_VERIFIED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::KeyValue::new( + MERCHANT_ID, + merchant_account.get_id().clone(), + )], + ); + } + + // If source verification is mandatory and source is not verified, fail with webhook authentication error + // else continue the flow + match ( + connector.is_webhook_source_verification_mandatory(), + source_verified, + ) { + (true, false) => Err(errors::ApiErrorResponse::WebhookAuthenticationFailed)?, + _ => { + event_object = connector + .get_webhook_resource_object(&request_details) + .switch() + .attach_printable("Could not find resource object in incoming webhook body")?; + + let webhook_details = api::IncomingWebhookDetails { + object_reference_id: object_ref_id.clone(), + resource_object: serde_json::to_vec(&event_object) + .change_context(errors::ParsingError::EncodeError("byte-vec")) + .attach_printable("Unable to convert webhook payload to a value") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "There was an issue when encoding the incoming webhook body to bytes", + )?, + }; + + match flow_type { + api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow( + state.clone(), + req_state, + merchant_account, + profile, + key_store, + webhook_details, + source_verified, + )) + .await + .attach_printable("Incoming webhook flow for payments failed")?, + + api::WebhookFlow::Refund => todo!(), + + api::WebhookFlow::Dispute => todo!(), + + api::WebhookFlow::BankTransfer => todo!(), + + api::WebhookFlow::ReturnResponse => WebhookResponseTracker::NoEffect, + + api::WebhookFlow::Mandate => todo!(), + + api::WebhookFlow::ExternalAuthentication => todo!(), + api::WebhookFlow::FraudCheck => todo!(), + + #[cfg(feature = "payouts")] + api::WebhookFlow::Payout => todo!(), + + api::WebhookFlow::Subscription => todo!(), + } + } + } + } else { + metrics::WEBHOOK_INCOMING_FILTERED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::KeyValue::new( + MERCHANT_ID, + merchant_account.get_id().get_string_repr().to_owned(), + )], + ); + WebhookResponseTracker::NoEffect + }; + + let response = connector + .get_webhook_api_response(&request_details) + .switch() + .attach_printable("Could not get incoming webhook api response from connector")?; + + let serialized_request = event_object + .masked_serialize() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not convert webhook effect to string")?; + Ok((response, webhook_effect, serialized_request)) +} + +#[instrument(skip_all)] +async fn payments_incoming_webhook_flow( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + webhook_details: api::IncomingWebhookDetails, + source_verified: bool, +) -> CustomResult { + let consume_or_trigger_flow = if source_verified { + payments::CallConnectorAction::HandleResponse(webhook_details.resource_object) + } else { + payments::CallConnectorAction::Trigger + }; + let key_manager_state = &(&state).into(); + let payments_response = match webhook_details.object_reference_id { + webhooks::ObjectReferenceId::PaymentId(id) => { + let get_trackers_response = get_trackers_response_for_payment_get_operation( + state.store.as_ref(), + &id, + profile.get_id(), + key_manager_state, + &key_store, + merchant_account.storage_scheme, + ) + .await?; + + let payment_id = get_trackers_response.payment_data.get_payment_id(); + + let lock_action = api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: payment_id.get_string_repr().to_owned(), + api_identifier: lock_utils::ApiIdentifier::Payments, + override_lock_retries: None, + }, + }; + + lock_action + .clone() + .perform_locking_action(&state, merchant_account.get_id().to_owned()) + .await?; + + let (payment_data, _req, customer, connector_http_status_code, external_latency) = + Box::pin(payments::payments_operation_core::< + api::PSync, + _, + _, + _, + PaymentStatusData, + >( + &state, + req_state, + merchant_account.clone(), + key_store.clone(), + profile, + payments::operations::PaymentGet, + api::PaymentsRetrieveRequest { + force_sync: true, + param: None, + }, + get_trackers_response, + consume_or_trigger_flow, + HeaderPayload::default(), + )) + .await?; + + let response = api_models::payments::PaymentsRetrieveResponse::generate_response( + payment_data, + customer, + &state.base_url, + payments::operations::PaymentGet, + &state.conf.connector_request_reference_id_config, + connector_http_status_code, + external_latency, + None, + &merchant_account, + ); + + lock_action + .free_lock_action(&state, merchant_account.get_id().to_owned()) + .await?; + + match response { + Ok(value) => value, + Err(err) + if matches!( + err.current_context(), + &errors::ApiErrorResponse::PaymentNotFound + ) && state + .clone() + .conf + .webhooks + .ignore_error + .payment_not_found + .unwrap_or(true) => + { + metrics::WEBHOOK_PAYMENT_NOT_FOUND.add( + &metrics::CONTEXT, + 1, + &add_attributes([("merchant_id", merchant_account.get_id().clone())]), + ); + return Ok(WebhookResponseTracker::NoEffect); + } + error @ Err(_) => error?, + } + } + _ => Err(errors::ApiErrorResponse::WebhookProcessingFailure).attach_printable( + "Did not get payment id as object reference id in webhook payments flow", + )?, + }; + + match payments_response { + services::ApplicationResponse::JsonWithHeaders((payments_response, _)) => { + let payment_id = payments_response.id.clone(); + + let status = payments_response.status; + + let event_type: Option = payments_response.status.foreign_into(); + + // If event is NOT an UnsupportedEvent, trigger Outgoing Webhook + if let Some(_outgoing_event_type) = event_type { + let _primary_object_created_at = payments_response.created; + // TODO: trigger an outgoing webhook to merchant + // Box::pin(super::create_event_and_trigger_outgoing_webhook( + // state, + // merchant_account, + // profile, + // &key_store, + // outgoing_event_type, + // enums::EventClass::Payments, + // payment_id.get_string_repr().to_owned(), + // enums::EventObjectType::PaymentDetails, + // api::OutgoingWebhookContent::PaymentDetails(Box::new(payments_response)), + // Some(primary_object_created_at), + // )) + // .await?; + }; + + let response = WebhookResponseTracker::Payment { payment_id, status }; + + Ok(response) + } + + _ => Err(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("received non-json response from payments core")?, + } +} + +async fn get_trackers_response_for_payment_get_operation( + db: &dyn StorageInterface, + payment_id: &api::PaymentIdType, + profile_id: &common_utils::id_type::ProfileId, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, +) -> errors::RouterResult>> +where + F: Clone, +{ + let (payment_intent, payment_attempt) = match payment_id { + api_models::payments::PaymentIdType::PaymentIntentId(ref id) => { + let payment_intent = db + .find_payment_intent_by_id( + key_manager_state, + id, + merchant_key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_attempt = db + .find_payment_attempt_by_id( + key_manager_state, + merchant_key_store, + &payment_intent + .active_attempt_id + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("active_attempt_id not present in payment_attempt")?, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + (payment_intent, payment_attempt) + } + api_models::payments::PaymentIdType::ConnectorTransactionId(ref id) => { + let payment_attempt = db + .find_payment_attempt_by_profile_id_connector_transaction_id( + key_manager_state, + merchant_key_store, + profile_id, + id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_intent = db + .find_payment_intent_by_id( + key_manager_state, + &payment_attempt.payment_id, + merchant_key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + (payment_intent, payment_attempt) + } + api_models::payments::PaymentIdType::PaymentAttemptId(ref id) => { + let global_attempt_id = common_utils::id_type::GlobalAttemptId::try_from( + std::borrow::Cow::Owned(id.to_owned()), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while getting GlobalAttemptId")?; + let payment_attempt = db + .find_payment_attempt_by_id( + key_manager_state, + merchant_key_store, + &global_attempt_id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_intent = db + .find_payment_intent_by_id( + key_manager_state, + &payment_attempt.payment_id, + merchant_key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + (payment_intent, payment_attempt) + } + api_models::payments::PaymentIdType::PreprocessingId(ref _id) => todo!(), + }; + Ok(payments::operations::GetTrackerResponse { + payment_data: PaymentStatusData { + flow: PhantomData, + payment_intent, + payment_attempt: Some(payment_attempt), + should_sync_with_connector: true, + }, + }) +} + +#[inline] +async fn verify_webhook_source_verification_call( + connector: ConnectorEnum, + state: &SessionState, + merchant_account: &domain::MerchantAccount, + merchant_connector_account: domain::MerchantConnectorAccount, + connector_name: &str, + request_details: &IncomingWebhookRequestDetails<'_>, +) -> CustomResult { + let connector_data = ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_name, + GetToken::Connector, + None, + ) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + .attach_printable("invalid connector name received in payment attempt")?; + let connector_integration: services::BoxedWebhookSourceVerificationConnectorIntegrationInterface< + hyperswitch_domain_models::router_flow_types::VerifyWebhookSource, + VerifyWebhookSourceRequestData, + VerifyWebhookSourceResponseData, + > = connector_data.connector.get_connector_integration(); + let connector_webhook_secrets = connector + .get_webhook_source_verification_merchant_secret( + merchant_account.get_id(), + connector_name, + merchant_connector_account.connector_webhook_details.clone(), + ) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let router_data = construct_webhook_router_data( + connector_name, + merchant_connector_account, + merchant_account, + &connector_webhook_secrets, + request_details, + ) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + .attach_printable("Failed while constructing webhook router data")?; + + let response = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await?; + + let verification_result = response + .response + .map(|response| response.verify_webhook_status); + match verification_result { + Ok(VerifyWebhookStatus::SourceVerified) => Ok(true), + _ => Ok(false), + } +} + +fn get_connector_by_connector_name( + state: &SessionState, + connector_name: &str, + merchant_connector_id: Option, +) -> CustomResult<(ConnectorEnum, String), errors::ApiErrorResponse> { + let authentication_connector = + api_models::enums::convert_authentication_connector(connector_name); + #[cfg(feature = "frm")] + { + let frm_connector = api_models::enums::convert_frm_connector(connector_name); + if frm_connector.is_some() { + let frm_connector_data = + api::FraudCheckConnectorData::get_connector_by_name(connector_name)?; + return Ok(( + frm_connector_data.connector, + frm_connector_data.connector_name.to_string(), + )); + } + } + + let (connector, connector_name) = if authentication_connector.is_some() { + let authentication_connector_data = + api::AuthenticationConnectorData::get_connector_by_name(connector_name)?; + ( + authentication_connector_data.connector, + authentication_connector_data.connector_name.to_string(), + ) + } else { + let connector_data = ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_name, + GetToken::Connector, + merchant_connector_id, + ) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "invalid connector name received".to_string(), + }) + .attach_printable("Failed construction of ConnectorData")?; + ( + connector_data.connector, + connector_data.connector_name.to_string(), + ) + }; + Ok((connector, connector_name)) +} + +/// This function fetches the merchant connector account and connector details +async fn fetch_mca_and_connector( + state: &SessionState, + connector_id: &common_utils::id_type::MerchantConnectorAccountId, + key_store: &domain::MerchantKeyStore, +) -> CustomResult<(domain::MerchantConnectorAccount, ConnectorEnum, String), errors::ApiErrorResponse> +{ + let db = &state.store; + let mca = db + .find_merchant_connector_account_by_id(&state.into(), connector_id, key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.get_string_repr().to_owned(), + }) + .attach_printable("error while fetching merchant_connector_account from connector_id")?; + + let (connector, connector_name) = + get_connector_by_connector_name(state, &mca.connector_name, Some(mca.get_id()))?; + + Ok((mca, connector, connector_name)) +} diff --git a/crates/router/src/core/webhooks/webhook_events.rs b/crates/router/src/core/webhooks/webhook_events.rs index 64b52b7995e..10e4b0eafb3 100644 --- a/crates/router/src/core/webhooks/webhook_events.rs +++ b/crates/router/src/core/webhooks/webhook_events.rs @@ -152,6 +152,7 @@ pub async fn list_delivery_attempts( } #[instrument(skip(state))] +#[cfg(feature = "v1")] pub async fn retry_delivery_attempt( state: SessionState, merchant_id: common_utils::id_type::MerchantId, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 93181c66142..a9df1abbc08 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1485,6 +1485,26 @@ impl PaymentAttemptInterface for KafkaStore { .await } + #[cfg(feature = "v2")] + async fn find_payment_attempt_by_profile_id_connector_transaction_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + profile_id: &id_type::ProfileId, + connector_transaction_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_profile_id_connector_transaction_id( + key_manager_state, + merchant_key_store, + profile_id, + connector_transaction_id, + storage_scheme, + ) + .await + } + #[cfg(feature = "v1")] async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id( &self, diff --git a/crates/router/src/events/outgoing_webhook_logs.rs b/crates/router/src/events/outgoing_webhook_logs.rs index db5a40fc6ea..64126ec76d9 100644 --- a/crates/router/src/events/outgoing_webhook_logs.rs +++ b/crates/router/src/events/outgoing_webhook_logs.rs @@ -26,10 +26,16 @@ pub struct OutgoingWebhookEvent { #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(tag = "outgoing_webhook_event_type", rename_all = "snake_case")] pub enum OutgoingWebhookEventContent { + #[cfg(feature = "v1")] Payment { payment_id: common_utils::id_type::PaymentId, content: Value, }, + #[cfg(feature = "v2")] + Payment { + payment_id: common_utils::id_type::GlobalPaymentId, + content: Value, + }, Payout { payout_id: String, content: Value, @@ -61,6 +67,8 @@ pub enum OutgoingWebhookEventContent { pub trait OutgoingWebhookEventMetric { fn get_outgoing_webhook_event_content(&self) -> Option; } + +#[cfg(feature = "v1")] impl OutgoingWebhookEventMetric for OutgoingWebhookContent { fn get_outgoing_webhook_event_content(&self) -> Option { match self { @@ -98,6 +106,44 @@ impl OutgoingWebhookEventMetric for OutgoingWebhookContent { } } +#[cfg(feature = "v2")] +impl OutgoingWebhookEventMetric for OutgoingWebhookContent { + fn get_outgoing_webhook_event_content(&self) -> Option { + match self { + Self::PaymentDetails(payment_payload) => Some(OutgoingWebhookEventContent::Payment { + payment_id: payment_payload.id.clone(), + content: masking::masked_serialize(&payment_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::RefundDetails(refund_payload) => Some(OutgoingWebhookEventContent::Refund { + payment_id: refund_payload.payment_id.clone(), + refund_id: refund_payload.get_refund_id_as_string(), + content: masking::masked_serialize(&refund_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::DisputeDetails(dispute_payload) => Some(OutgoingWebhookEventContent::Dispute { + payment_id: dispute_payload.payment_id.clone(), + attempt_id: dispute_payload.attempt_id.clone(), + dispute_id: dispute_payload.dispute_id.clone(), + content: masking::masked_serialize(&dispute_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::MandateDetails(mandate_payload) => Some(OutgoingWebhookEventContent::Mandate { + payment_method_id: mandate_payload.payment_method_id.clone(), + mandate_id: mandate_payload.mandate_id.clone(), + content: masking::masked_serialize(&mandate_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + #[cfg(feature = "payouts")] + Self::PayoutDetails(payout_payload) => Some(OutgoingWebhookEventContent::Payout { + payout_id: payout_payload.payout_id.clone(), + content: masking::masked_serialize(&payout_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + } + } +} + impl OutgoingWebhookEvent { #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 7efaab21d5b..829216db1dd 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -138,7 +138,8 @@ pub fn mk_app( .service(routes::Payments::server(state.clone())) .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) - .service(routes::MerchantConnectorAccount::server(state.clone())); + .service(routes::MerchantConnectorAccount::server(state.clone())) + .service(routes::Webhooks::server(state.clone())); #[cfg(feature = "v1")] { diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 0f1529a9bdd..aaadcaa4eb5 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -56,7 +56,6 @@ pub mod verification; pub mod verify_connector; #[cfg(all(feature = "olap", feature = "v1"))] pub mod webhook_events; -#[cfg(feature = "v1")] pub mod webhooks; #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 892dfa62a36..5c81db35ea9 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -48,7 +48,7 @@ use super::poll; use super::routing; #[cfg(all(feature = "olap", feature = "v1"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; -#[cfg(all(feature = "oltp", feature = "v1"))] +#[cfg(feature = "oltp")] use super::webhooks::*; use super::{ admin, api_keys, cache::*, connector_onboarding, disputes, files, gsm, health::*, profiles, @@ -1437,6 +1437,29 @@ impl Webhooks { } } +#[cfg(all(feature = "oltp", feature = "v2"))] +impl Webhooks { + pub fn server(config: AppState) -> Scope { + use api_models::webhooks as webhook_type; + + #[allow(unused_mut)] + let mut route = web::scope("/v2/webhooks") + .app_data(web::Data::new(config)) + .service( + web::resource("/{merchant_id}/{profile_id}/{connector_id}") + .route( + web::post().to(receive_incoming_webhook::), + ) + .route(web::get().to(receive_incoming_webhook::)) + .route( + web::put().to(receive_incoming_webhook::), + ), + ); + + route + } +} + pub struct Configs; #[cfg(any(feature = "olap", feature = "oltp"))] diff --git a/crates/router/src/routes/webhook_events.rs b/crates/router/src/routes/webhook_events.rs index 5039f72db31..08427012ce4 100644 --- a/crates/router/src/routes/webhook_events.rs +++ b/crates/router/src/routes/webhook_events.rs @@ -92,6 +92,7 @@ pub async fn list_webhook_delivery_attempts( } #[instrument(skip_all, fields(flow = ?Flow::WebhookEventDeliveryRetry))] +#[cfg(feature = "v1")] pub async fn retry_webhook_delivery_attempt( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/routes/webhooks.rs b/crates/router/src/routes/webhooks.rs index 43b50b04601..5427ac34b4e 100644 --- a/crates/router/src/routes/webhooks.rs +++ b/crates/router/src/routes/webhooks.rs @@ -11,6 +11,7 @@ use crate::{ }; #[instrument(skip_all, fields(flow = ?Flow::IncomingWebhookReceive))] +#[cfg(feature = "v1")] pub async fn receive_incoming_webhook( state: web::Data, req: HttpRequest, @@ -42,3 +43,45 @@ pub async fn receive_incoming_webhook( )) .await } + +#[instrument(skip_all, fields(flow = ?Flow::IncomingWebhookReceive))] +#[cfg(feature = "v2")] +pub async fn receive_incoming_webhook( + state: web::Data, + req: HttpRequest, + body: web::Bytes, + path: web::Path<( + common_utils::id_type::MerchantId, + common_utils::id_type::ProfileId, + common_utils::id_type::MerchantConnectorAccountId, + )>, +) -> impl Responder { + let flow = Flow::IncomingWebhookReceive; + let (merchant_id, profile_id, connector_id) = path.into_inner(); + + Box::pin(api::server_wrap( + flow.clone(), + state, + &req, + (), + |state, auth, _, req_state| { + webhooks::incoming_webhooks_wrapper::( + &flow, + state.to_owned(), + req_state, + &req, + auth.merchant_account, + auth.profile, + auth.key_store, + &connector_id, + body.clone(), + ) + }, + &auth::MerchantIdAndProfileIdAuth { + merchant_id, + profile_id, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index d4ac3bc846d..2541a5dc7d4 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1403,6 +1403,65 @@ where } } +#[derive(Debug)] +#[cfg(feature = "v2")] +pub struct MerchantIdAndProfileIdAuth { + pub merchant_id: id_type::MerchantId, + pub profile_id: id_type::ProfileId, +} + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for MerchantIdAndProfileIdAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &self.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &self.merchant_id, + &self.profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &self.merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantId { + merchant_id: auth.merchant_account.get_id().clone(), + }, + )) + } +} + #[derive(Debug)] #[cfg(feature = "v2")] pub struct PublishableKeyAndProfileIdAuth { diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 57ef1d3336f..524712f1ca0 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -45,12 +45,19 @@ pub use super::payments_v2::{ use crate::core::errors; pub trait PaymentIdTypeExt { + #[cfg(feature = "v1")] fn get_payment_intent_id( &self, ) -> errors::CustomResult; + + #[cfg(feature = "v2")] + fn get_payment_intent_id( + &self, + ) -> errors::CustomResult; } impl PaymentIdTypeExt for PaymentIdType { + #[cfg(feature = "v1")] fn get_payment_intent_id( &self, ) -> errors::CustomResult { @@ -64,6 +71,21 @@ impl PaymentIdTypeExt for PaymentIdType { .attach_printable("Expected payment intent ID but got connector transaction ID"), } } + + #[cfg(feature = "v2")] + fn get_payment_intent_id( + &self, + ) -> errors::CustomResult { + match self { + Self::PaymentIntentId(id) => Ok(id.clone()), + Self::ConnectorTransactionId(_) + | Self::PaymentAttemptId(_) + | Self::PreprocessingId(_) => Err(errors::ValidationError::IncorrectValueProvided { + field_name: "payment_id", + }) + .attach_printable("Expected payment intent ID but got connector transaction ID"), + } + } } pub(crate) trait MandateValidationFieldsExt { diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index ae4a9e659a6..83691f46129 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -103,6 +103,19 @@ impl PaymentAttemptInterface for MockDb { Err(StorageError::MockDbError)? } + #[cfg(feature = "v2")] + async fn find_payment_attempt_by_profile_id_connector_transaction_id( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &MerchantKeyStore, + _profile_id: &id_type::ProfileId, + _connector_transaction_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + // [#172]: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } + #[cfg(feature = "v1")] async fn find_attempts_by_merchant_id_payment_id( &self, diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 7bc9bcc8a92..9b9121952b1 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -215,8 +215,8 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } - #[cfg(feature = "v1")] #[instrument(skip_all)] + #[cfg(feature = "v1")] async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &common_utils::id_type::MerchantId, @@ -237,6 +237,36 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] + #[cfg(feature = "v2")] + async fn find_payment_attempt_by_profile_id_connector_transaction_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + connector_txn_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = pg_connection_read(self).await?; + DieselPaymentAttempt::find_by_profile_id_connector_transaction_id( + &conn, + profile_id, + connector_txn_id, + ) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + })? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id( @@ -933,8 +963,29 @@ impl PaymentAttemptInterface for KVRouterStore { } } - #[cfg(feature = "v1")] + #[cfg(feature = "v2")] + async fn find_payment_attempt_by_profile_id_connector_transaction_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + connector_transaction_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + // Ignoring storage scheme for v2 implementation + self.router_store + .find_payment_attempt_by_profile_id_connector_transaction_id( + key_manager_state, + merchant_key_store, + profile_id, + connector_transaction_id, + storage_scheme, + ) + .await + } + #[instrument(skip_all)] + #[cfg(feature = "v1")] async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &common_utils::id_type::MerchantId, From bbd55e32f838349b402e8cd0abc06d34f647be94 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:14:52 +0530 Subject: [PATCH 07/16] refactor(users): Force 2FA in production environment (#6596) --- config/deployments/production.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 73e5794f042..76f42085f92 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -149,7 +149,7 @@ password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Production" base_url = "https://live.hyperswitch.io" -force_two_factor_auth = false +force_two_factor_auth = true [frm] enabled = false From e730a2ee5a35d56f3740e923cb16de67edca2fc0 Mon Sep 17 00:00:00 2001 From: Kashif Date: Tue, 19 Nov 2024 17:43:54 +0530 Subject: [PATCH 08/16] fix(connector): [Worldpay] use 4 digit expiry year (#6543) --- .../src/connectors/worldpay/transformers.rs | 6 +-- crates/hyperswitch_connectors/src/utils.rs | 9 +++++ .../cypress/e2e/PaymentUtils/WorldPay.js | 40 +++++++++---------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index fba589a4895..757760965e7 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -25,7 +25,7 @@ use super::{requests::*, response::*}; use crate::{ types::ResponseRouterData, utils::{ - self, AddressData, ForeignTryFrom, PaymentsAuthorizeRequestData, + self, AddressData, CardData, ForeignTryFrom, PaymentsAuthorizeRequestData, PaymentsSetupMandateRequestData, RouterData as RouterDataTrait, }, }; @@ -77,8 +77,8 @@ fn fetch_payment_instrument( PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment { payment_type: PaymentType::Plain, expiry_date: ExpiryDate { - month: utils::CardData::get_expiry_month_as_i8(&card)?, - year: utils::CardData::get_expiry_year_as_i32(&card)?, + month: card.get_expiry_month_as_i8()?, + year: card.get_expiry_year_as_4_digit_i32()?, }, card_number: card.card_number, cvc: card.card_cvc, diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 14596c0f2a2..d928bbfa4eb 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -844,6 +844,7 @@ pub trait CardData { fn get_expiry_date_as_mmyy(&self) -> Result, errors::ConnectorError>; fn get_expiry_month_as_i8(&self) -> Result, Error>; fn get_expiry_year_as_i32(&self) -> Result, Error>; + fn get_expiry_year_as_4_digit_i32(&self) -> Result, Error>; } impl CardData for Card { @@ -922,6 +923,14 @@ impl CardData for Card { .change_context(errors::ConnectorError::ResponseDeserializationFailed) .map(Secret::new) } + fn get_expiry_year_as_4_digit_i32(&self) -> Result, Error> { + self.get_expiry_year_4_digit() + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } } #[track_caller] diff --git a/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js b/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js index 70ed4fefa6c..5b950219fb2 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js @@ -26,23 +26,23 @@ const browser_info = { "ip_address": "127.0.0.1" }; -const successfulNo3DSCardDetails = { +const successfulNoThreeDsCardDetailsRequest = { card_number: "4242424242424242", card_exp_month: "10", - card_exp_year: "2030", + card_exp_year: "30", card_holder_name: "morino", card_cvc: "737", }; -const successfulThreeDSTestCardDetails = { +const successfulThreeDsTestCardDetailsRequest = { card_number: "4000000000001091", card_exp_month: "10", - card_exp_year: "2030", + card_exp_year: "30", card_holder_name: "morino", card_cvc: "737", }; -const payment_method_data_no3ds = { +const paymentMethodDataNoThreeDsResponse = { card: { last4: "4242", card_type: "CREDIT", @@ -52,7 +52,7 @@ const payment_method_data_no3ds = { card_isin: "424242", card_extended_bin: null, card_exp_month: "10", - card_exp_year: "2030", + card_exp_year: "30", card_holder_name: null, payment_checks: null, authentication_data: null @@ -70,7 +70,7 @@ const payment_method_data_3ds = { card_isin: "400000", card_extended_bin: null, card_exp_month: "10", - card_exp_year: "2030", + card_exp_year: "30", card_holder_name: null, payment_checks: null, authentication_data: null @@ -115,7 +115,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", customer_acceptance: null, @@ -129,7 +129,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", attempt_count: 1, - payment_method_data: payment_method_data_no3ds, + payment_method_data: paymentMethodDataNoThreeDsResponse, }, }, }, @@ -138,7 +138,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", customer_acceptance: null, @@ -151,7 +151,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", attempt_count: 1, - payment_method_data: payment_method_data_no3ds, + payment_method_data: paymentMethodDataNoThreeDsResponse, }, }, }, @@ -160,7 +160,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", customer_acceptance: null, @@ -179,7 +179,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", customer_acceptance: null, @@ -222,7 +222,7 @@ export const connectorDetails = { Request: { payment_method: "card", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", setup_future_usage: "on_session", @@ -250,7 +250,7 @@ export const connectorDetails = { Request: { payment_method: "card", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", setup_future_usage: "on_session", @@ -276,7 +276,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", payment_method_data: { - card: successfulThreeDSTestCardDetails, + card: successfulThreeDsTestCardDetailsRequest, }, currency: "USD", customer_acceptance: null, @@ -297,7 +297,7 @@ export const connectorDetails = { payment_method: "card", payment_method_type: "debit", payment_method_data: { - card: successfulThreeDSTestCardDetails, + card: successfulThreeDsTestCardDetailsRequest, }, currency: "USD", customer_acceptance: null, @@ -322,7 +322,7 @@ export const connectorDetails = { Request: { payment_method: "card", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "EUR", customer_acceptance: null, @@ -344,7 +344,7 @@ export const connectorDetails = { Request: { payment_method: "card", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", customer_acceptance: null, @@ -417,7 +417,7 @@ export const connectorDetails = { Request: { payment_method: "card", payment_method_data: { - card: successfulNo3DSCardDetails, + card: successfulNoThreeDsCardDetailsRequest, }, currency: "USD", mandate_data: singleUseMandateData, From 11e92413b22f13df8cfa62020d48d490e37b5d87 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:33:30 +0530 Subject: [PATCH 09/16] refactor(payment_methods_v2): rename `payment_method` and `payment_method_type` fields and use concrete type for `payment_method_data` (#6555) --- api-reference-v2/openapi_spec.json | 557 ++++++++++-------- api-reference/openapi_spec.json | 243 ++++++-- crates/api_models/src/events.rs | 4 +- crates/api_models/src/events/payment.rs | 13 + crates/api_models/src/payment_methods.rs | 119 ++-- crates/api_models/src/payments.rs | 1 + crates/diesel_models/src/payment_method.rs | 78 ++- crates/diesel_models/src/schema_v2.rs | 7 +- .../src/payment_methods.rs | 55 +- .../src/type_encryption.rs | 322 +++++++++- crates/openapi/Cargo.toml | 2 +- crates/openapi/src/openapi.rs | 8 +- crates/openapi/src/openapi_v2.rs | 12 +- crates/router/src/core/customers.rs | 4 +- crates/router/src/core/locker_migration.rs | 14 +- crates/router/src/core/payment_methods.rs | 61 +- .../router/src/core/payment_methods/cards.rs | 66 ++- .../surcharge_decision_configs.rs | 5 +- .../src/core/payment_methods/transformers.rs | 10 +- crates/router/src/core/payments.rs | 4 +- crates/router/src/core/payments/helpers.rs | 24 +- .../payments/operations/payment_confirm.rs | 2 +- .../router/src/core/payments/tokenization.rs | 6 +- crates/router/src/core/pm_auth.rs | 6 +- crates/router/src/routes/payment_methods.rs | 2 +- crates/router/src/types/api/mandates.rs | 4 +- .../router/src/types/api/payment_methods.rs | 12 +- crates/router/src/types/domain/types.rs | 2 +- crates/router/src/types/transformers.rs | 4 +- .../down.sql | 9 +- .../up.sql | 9 +- 31 files changed, 1169 insertions(+), 496 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index f00998f154f..53b8c636dab 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3237,6 +3237,27 @@ ], "description": "Masked payout method details for bank payout method" }, + "BankCodeResponse": { + "type": "object", + "required": [ + "bank_name", + "eligible_connectors" + ], + "properties": { + "bank_name": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BankNames" + } + }, + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "BankDebitAdditionalData": { "oneOf": [ { @@ -3507,6 +3528,20 @@ } ] }, + "BankDebitTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "BankHolderType": { "type": "string", "enum": [ @@ -4730,6 +4765,25 @@ } ] }, + "BankTransferTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of eligible connectors for a given payment experience", + "example": [ + "stripe", + "adyen" + ] + } + } + }, "BankType": { "type": "string", "enum": [ @@ -5284,7 +5338,8 @@ "card_number", "card_exp_month", "card_exp_year", - "card_holder_name" + "card_holder_name", + "card_issuing_country" ], "properties": { "card_number": { @@ -5314,9 +5369,7 @@ "nullable": true }, "card_issuing_country": { - "type": "string", - "description": "Card Issuing Country", - "nullable": true + "$ref": "#/components/schemas/CountryAlpha2" }, "card_network": { "allOf": [ @@ -5332,8 +5385,11 @@ "nullable": true }, "card_type": { - "type": "string", - "description": "Card Type", + "allOf": [ + { + "$ref": "#/components/schemas/CardType" + } + ], "nullable": true } }, @@ -5345,12 +5401,12 @@ "saved_to_locker" ], "properties": { - "scheme": { - "type": "string", - "nullable": true - }, "issuer_country": { - "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], "nullable": true }, "last4_digits": { @@ -5365,10 +5421,6 @@ "type": "string", "nullable": true }, - "card_token": { - "type": "string", - "nullable": true - }, "card_holder_name": { "type": "string", "nullable": true @@ -5409,21 +5461,9 @@ "CardDetailUpdate": { "type": "object", "required": [ - "card_exp_month", - "card_exp_year", "card_holder_name" ], "properties": { - "card_exp_month": { - "type": "string", - "description": "Card Expiry Month", - "example": "10" - }, - "card_exp_year": { - "type": "string", - "description": "Card Expiry Year", - "example": "25" - }, "card_holder_name": { "type": "string", "description": "Card Holder Name", @@ -5455,6 +5495,41 @@ "Maestro" ] }, + "CardNetworkTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of eligible connectors for a given card network", + "example": [ + "stripe", + "adyen" + ] + } + } + }, "CardPayout": { "type": "object", "required": [ @@ -5648,6 +5723,13 @@ } ] }, + "CardType": { + "type": "string", + "enum": [ + "credit", + "debit" + ] + }, "CashappQr": { "type": "object" }, @@ -6740,20 +6822,19 @@ "CustomerPaymentMethod": { "type": "object", "required": [ - "payment_token", "payment_method_id", "customer_id", - "payment_method", + "payment_method_type", "recurring_enabled", - "installment_payment_enabled", "requires_cvv", - "default_payment_method_set" + "is_default" ], "properties": { "payment_token": { "type": "string", "description": "Token for payment method in temporary card locker which gets refreshed often", - "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" + "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef", + "nullable": true }, "payment_method_id": { "type": "string", @@ -6767,10 +6848,10 @@ "maxLength": 64, "minLength": 1 }, - "payment_method": { + "payment_method_type": { "$ref": "#/components/schemas/PaymentMethod" }, - "payment_method_type": { + "payment_method_subtype": { "allOf": [ { "$ref": "#/components/schemas/PaymentMethodType" @@ -6778,54 +6859,27 @@ ], "nullable": true }, - "payment_method_issuer": { - "type": "string", - "description": "The name of the bank/ provider issuing the payment method to the end user", - "example": "Citibank", - "nullable": true - }, - "payment_method_issuer_code": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodIssuerCode" - } - ], - "nullable": true - }, "recurring_enabled": { "type": "boolean", "description": "Indicates whether the payment method is eligible for recurring payments", "example": true }, - "installment_payment_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for installment payments", - "example": true - }, - "payment_experience": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentExperience" - }, - "description": "Type of payment experience enabled with the connector", - "example": [ - "redirect_to_url" + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodListData" + } ], "nullable": true }, - "card": { + "bank": { "allOf": [ { - "$ref": "#/components/schemas/CardDetailFromLocker" + "$ref": "#/components/schemas/MaskedBankDetails" } ], "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, "created": { "type": "string", "format": "date-time", @@ -6833,22 +6887,6 @@ "example": "2023-01-18T11:04:09.922Z", "nullable": true }, - "bank_transfer": { - "allOf": [ - { - "$ref": "#/components/schemas/Bank" - } - ], - "nullable": true - }, - "bank": { - "allOf": [ - { - "$ref": "#/components/schemas/MaskedBankDetails" - } - ], - "nullable": true - }, "surcharge_details": { "allOf": [ { @@ -6869,7 +6907,7 @@ "example": "2024-02-24T11:04:09.922Z", "nullable": true }, - "default_payment_method_set": { + "is_default": { "type": "boolean", "description": "Indicates if the payment method has been set to default or not", "example": true @@ -11940,6 +11978,33 @@ "collect_otp" ] }, + "PaymentExperienceTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "payment_experience_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], + "nullable": true + }, + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of eligible connectors for a given payment experience", + "example": [ + "stripe", + "adyen" + ] + } + } + }, "PaymentLinkConfig": { "type": "object", "required": [ @@ -12356,41 +12421,17 @@ "PaymentMethodCreate": { "type": "object", "required": [ - "payment_method" + "payment_method_type", + "payment_method_subtype", + "customer_id", + "payment_method_data" ], "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" - }, "payment_method_type": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodType" - } - ], - "nullable": true - }, - "payment_method_issuer": { - "type": "string", - "description": "The name of the bank/ provider issuing the payment method to the end user", - "example": "Citibank", - "nullable": true - }, - "payment_method_issuer_code": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodIssuerCode" - } - ], - "nullable": true + "$ref": "#/components/schemas/PaymentMethod" }, - "card": { - "allOf": [ - { - "$ref": "#/components/schemas/CardDetail" - } - ], - "nullable": true + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" }, "metadata": { "type": "object", @@ -12401,44 +12442,11 @@ "type": "string", "description": "The unique identifier of the customer.", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, "maxLength": 64, "minLength": 1 }, - "card_network": { - "type": "string", - "description": "The card network", - "example": "Visa", - "nullable": true - }, - "bank_transfer": { - "allOf": [ - { - "$ref": "#/components/schemas/Bank" - } - ], - "nullable": true - }, - "wallet": { - "allOf": [ - { - "$ref": "#/components/schemas/Wallet" - } - ], - "nullable": true - }, - "client_secret": { - "type": "string", - "description": "For Client based calls, SDK will use the client_secret\nin order to call /payment_methods\nClient secret will be generated whenever a new\npayment method is created", - "nullable": true - }, "payment_method_data": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodCreateData" - } - ], - "nullable": true + "$ref": "#/components/schemas/PaymentMethodCreateData" }, "billing": { "allOf": [ @@ -12909,19 +12917,13 @@ "PaymentMethodDeleteResponse": { "type": "object", "required": [ - "payment_method_id", - "deleted" + "payment_method_id" ], "properties": { "payment_method_id": { "type": "string", "description": "The unique identifier of the Payment method", "example": "card_rGK4Vi5iSW70MY7J2mIg" - }, - "deleted": { - "type": "boolean", - "description": "Whether payment method was deleted or not", - "example": true } } }, @@ -12940,27 +12942,31 @@ "jp_bacs" ] }, - "PaymentMethodList": { - "type": "object", - "required": [ - "payment_method" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" + "PaymentMethodListData": { + "oneOf": [ + { + "type": "object", + "required": [ + "card" + ], + "properties": { + "card": { + "$ref": "#/components/schemas/CardDetailFromLocker" + } + } }, - "payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "This is a sub-category of payment method.", - "example": [ - "credit" + { + "type": "object", + "required": [ + "bank" ], - "nullable": true + "properties": { + "bank": { + "$ref": "#/components/schemas/Bank" + } + } } - } + ] }, "PaymentMethodListResponse": { "type": "object", @@ -12985,19 +12991,9 @@ "payment_methods": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentMethodList" + "$ref": "#/components/schemas/ResponsePaymentMethodsEnabled" }, - "description": "Information about the payment method", - "example": [ - { - "payment_experience": null, - "payment_method": "wallet", - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ] - } - ] + "description": "Information about the payment method" }, "mandate_payment": { "$ref": "#/components/schemas/MandateType" @@ -13044,9 +13040,8 @@ "required": [ "merchant_id", "payment_method_id", - "payment_method", - "recurring_enabled", - "installment_payment_enabled" + "payment_method_type", + "recurring_enabled" ], "properties": { "merchant_id": { @@ -13067,10 +13062,10 @@ "description": "The unique identifier of the Payment method", "example": "card_rGK4Vi5iSW70MY7J2mIg" }, - "payment_method": { + "payment_method_type": { "$ref": "#/components/schemas/PaymentMethod" }, - "payment_method_type": { + "payment_method_subtype": { "allOf": [ { "$ref": "#/components/schemas/PaymentMethodType" @@ -13078,40 +13073,11 @@ ], "nullable": true }, - "card": { - "allOf": [ - { - "$ref": "#/components/schemas/CardDetailFromLocker" - } - ], - "nullable": true - }, "recurring_enabled": { "type": "boolean", "description": "Indicates whether the payment method is eligible for recurring payments", "example": true }, - "installment_payment_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for installment payments", - "example": true - }, - "payment_experience": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentExperience" - }, - "description": "Type of payment experience enabled with the connector", - "example": [ - "redirect_to_url" - ], - "nullable": true - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, "created": { "type": "string", "format": "date-time", @@ -13119,14 +13085,6 @@ "example": "2023-01-18T11:04:09.922Z", "nullable": true }, - "bank_transfer": { - "allOf": [ - { - "$ref": "#/components/schemas/Bank" - } - ], - "nullable": true - }, "last_used_at": { "type": "string", "format": "date-time", @@ -13137,9 +13095,32 @@ "type": "string", "description": "For Client based calls", "nullable": true + }, + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodResponseData" + } + ], + "nullable": true } } }, + "PaymentMethodResponseData": { + "oneOf": [ + { + "type": "object", + "required": [ + "card" + ], + "properties": { + "card": { + "$ref": "#/components/schemas/CardDetailFromLocker" + } + } + } + ] + }, "PaymentMethodStatus": { "type": "string", "description": "Payment Method Status", @@ -13253,14 +13234,12 @@ }, "PaymentMethodUpdate": { "type": "object", + "required": [ + "payment_method_data" + ], "properties": { - "card": { - "allOf": [ - { - "$ref": "#/components/schemas/CardDetailUpdate" - } - ], - "nullable": true + "payment_method_data": { + "$ref": "#/components/schemas/PaymentMethodUpdateData" }, "client_secret": { "type": "string", @@ -13273,6 +13252,21 @@ }, "additionalProperties": false }, + "PaymentMethodUpdateData": { + "oneOf": [ + { + "type": "object", + "required": [ + "card" + ], + "properties": { + "card": { + "$ref": "#/components/schemas/CardDetailUpdate" + } + } + } + ] + }, "PaymentMethodsEnabled": { "type": "object", "description": "Details of all the payment methods enabled for the connector for the given merchant account", @@ -17829,6 +17823,97 @@ } } }, + "ResponsePaymentMethodTypes": { + "type": "object", + "required": [ + "payment_method_subtype" + ], + "properties": { + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_experience": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentExperienceTypes" + }, + "description": "The list of payment experiences enabled, if applicable for a payment method type", + "nullable": true + }, + "card_networks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardNetworkTypes" + }, + "description": "The list of card networks enabled, if applicable for a payment method type", + "nullable": true + }, + "bank_names": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BankCodeResponse" + }, + "description": "The list of banks enabled, if applicable for a payment method type", + "nullable": true + }, + "bank_debits": { + "allOf": [ + { + "$ref": "#/components/schemas/BankDebitTypes" + } + ], + "nullable": true + }, + "bank_transfers": { + "allOf": [ + { + "$ref": "#/components/schemas/BankTransferTypes" + } + ], + "nullable": true + }, + "required_fields": { + "type": "object", + "description": "Required fields for the payment_method_type.", + "additionalProperties": { + "$ref": "#/components/schemas/RequiredFieldInfo" + }, + "nullable": true + }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, + "pm_auth_connector": { + "type": "string", + "description": "auth service connector label for this payment method type, if exists", + "nullable": true + } + } + }, + "ResponsePaymentMethodsEnabled": { + "type": "object", + "required": [ + "payment_method", + "payment_method_types" + ], + "properties": { + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponsePaymentMethodTypes" + }, + "description": "The list of payment method types enabled for a connector account" + } + } + }, "RetrieveApiKeyResponse": { "type": "object", "description": "The response body for retrieving an API Key.", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d4c5717d215..1ba6ac636b8 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -6144,6 +6144,27 @@ ], "description": "Masked payout method details for bank payout method" }, + "BankCodeResponse": { + "type": "object", + "required": [ + "bank_name", + "eligible_connectors" + ], + "properties": { + "bank_name": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BankNames" + } + }, + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "BankDebitAdditionalData": { "oneOf": [ { @@ -6414,6 +6435,20 @@ } ] }, + "BankDebitTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "BankHolderType": { "type": "string", "enum": [ @@ -7637,6 +7672,25 @@ } ] }, + "BankTransferTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of eligible connectors for a given payment experience", + "example": [ + "stripe", + "adyen" + ] + } + } + }, "BankType": { "type": "string", "enum": [ @@ -8362,6 +8416,41 @@ "Maestro" ] }, + "CardNetworkTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of eligible connectors for a given card network", + "example": [ + "stripe", + "adyen" + ] + } + } + }, "CardPayout": { "type": "object", "required": [ @@ -15066,6 +15155,33 @@ "collect_otp" ] }, + "PaymentExperienceTypes": { + "type": "object", + "required": [ + "eligible_connectors" + ], + "properties": { + "payment_experience_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], + "nullable": true + }, + "eligible_connectors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of eligible connectors for a given payment experience", + "example": [ + "stripe", + "adyen" + ] + } + } + }, "PaymentLinkConfig": { "type": "object", "required": [ @@ -16066,28 +16182,6 @@ "jp_bacs" ] }, - "PaymentMethodList": { - "type": "object", - "required": [ - "payment_method" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "This is a sub-category of payment method.", - "example": [ - "credit" - ], - "nullable": true - } - } - }, "PaymentMethodListResponse": { "type": "object", "required": [ @@ -16111,19 +16205,9 @@ "payment_methods": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentMethodList" + "$ref": "#/components/schemas/ResponsePaymentMethodsEnabled" }, - "description": "Information about the payment method", - "example": [ - { - "payment_experience": null, - "payment_method": "wallet", - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ] - } - ] + "description": "Information about the payment method" }, "mandate_payment": { "$ref": "#/components/schemas/MandateType" @@ -22387,6 +22471,97 @@ } } }, + "ResponsePaymentMethodTypes": { + "type": "object", + "required": [ + "payment_method_type" + ], + "properties": { + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_experience": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentExperienceTypes" + }, + "description": "The list of payment experiences enabled, if applicable for a payment method type", + "nullable": true + }, + "card_networks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardNetworkTypes" + }, + "description": "The list of card networks enabled, if applicable for a payment method type", + "nullable": true + }, + "bank_names": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BankCodeResponse" + }, + "description": "The list of banks enabled, if applicable for a payment method type", + "nullable": true + }, + "bank_debits": { + "allOf": [ + { + "$ref": "#/components/schemas/BankDebitTypes" + } + ], + "nullable": true + }, + "bank_transfers": { + "allOf": [ + { + "$ref": "#/components/schemas/BankTransferTypes" + } + ], + "nullable": true + }, + "required_fields": { + "type": "object", + "description": "Required fields for the payment_method_type.", + "additionalProperties": { + "$ref": "#/components/schemas/RequiredFieldInfo" + }, + "nullable": true + }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, + "pm_auth_connector": { + "type": "string", + "description": "auth service connector label for this payment method type, if exists", + "nullable": true + } + } + }, + "ResponsePaymentMethodsEnabled": { + "type": "object", + "required": [ + "payment_method", + "payment_method_types" + ], + "properties": { + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponsePaymentMethodTypes" + }, + "description": "The list of payment method types enabled for a connector account" + } + } + }, "RetrieveApiKeyResponse": { "type": "object", "description": "The response body for retrieving an API Key.", diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 77d8cb117ef..dad624ef87c 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -178,8 +178,8 @@ impl ApiEventMetric for PaymentMethodIntentConfirmInternal { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::PaymentMethod { payment_method_id: self.id.clone(), - payment_method: Some(self.payment_method), - payment_method_type: Some(self.payment_method_type), + payment_method: Some(self.payment_method_type), + payment_method_type: Some(self.payment_method_subtype), }) } } diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index a3e36a232dc..b1f15188dcb 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -196,6 +196,10 @@ impl ApiEventMetric for PaymentsResponse { } impl ApiEventMetric for PaymentMethodResponse { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] fn get_api_event_type(&self) -> Option { Some(ApiEventsType::PaymentMethod { payment_method_id: self.payment_method_id.clone(), @@ -203,6 +207,15 @@ impl ApiEventMetric for PaymentMethodResponse { payment_method_type: self.payment_method_type, }) } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_id.clone(), + payment_method: self.payment_method_type, + payment_method_type: self.payment_method_subtype, + }) + } } impl ApiEventMetric for PaymentMethodUpdate {} diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 0602f69d158..5baf138b090 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -110,11 +110,11 @@ pub struct PaymentMethodCreate { pub struct PaymentMethodCreate { /// The type of payment method use for the payment. #[schema(value_type = PaymentMethod,example = "card")] - pub payment_method: api_enums::PaymentMethod, + pub payment_method_type: api_enums::PaymentMethod, /// This is a sub-category of payment method. #[schema(value_type = PaymentMethodType,example = "credit")] - pub payment_method_type: api_enums::PaymentMethodType, + pub payment_method_subtype: api_enums::PaymentMethodType, /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] @@ -165,20 +165,20 @@ pub struct PaymentMethodIntentConfirm { /// The type of payment method use for the payment. #[schema(value_type = PaymentMethod,example = "card")] - pub payment_method: api_enums::PaymentMethod, + pub payment_method_type: api_enums::PaymentMethod, /// This is a sub-category of payment method. #[schema(value_type = PaymentMethodType,example = "credit")] - pub payment_method_type: api_enums::PaymentMethodType, + pub payment_method_subtype: api_enums::PaymentMethodType, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl PaymentMethodIntentConfirm { pub fn validate_payment_method_data_against_payment_method( - payment_method: api_enums::PaymentMethod, + payment_method_type: api_enums::PaymentMethod, payment_method_data: PaymentMethodCreateData, ) -> bool { - match payment_method { + match payment_method_type { api_enums::PaymentMethod::Card => { matches!(payment_method_data, PaymentMethodCreateData::Card(_)) } @@ -195,11 +195,11 @@ pub struct PaymentMethodIntentConfirmInternal { pub id: String, /// The type of payment method use for the payment. #[schema(value_type = PaymentMethod,example = "card")] - pub payment_method: api_enums::PaymentMethod, + pub payment_method_type: api_enums::PaymentMethod, /// This is a sub-category of payment method. #[schema(value_type = PaymentMethodType,example = "credit")] - pub payment_method_type: api_enums::PaymentMethodType, + pub payment_method_subtype: api_enums::PaymentMethodType, /// For SDK based calls, client_secret would be required pub client_secret: String, @@ -217,8 +217,8 @@ impl From for PaymentMethodIntentConfirm { fn from(item: PaymentMethodIntentConfirmInternal) -> Self { Self { client_secret: item.client_secret, - payment_method: item.payment_method, payment_method_type: item.payment_method_type, + payment_method_subtype: item.payment_method_subtype, customer_id: item.customer_id, payment_method_data: item.payment_method_data.clone(), } @@ -339,10 +339,10 @@ impl PaymentMethodCreate { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl PaymentMethodCreate { pub fn validate_payment_method_data_against_payment_method( - payment_method: api_enums::PaymentMethod, + payment_method_type: api_enums::PaymentMethod, payment_method_data: PaymentMethodCreateData, ) -> bool { - match payment_method { + match payment_method_type { api_enums::PaymentMethod::Card => { matches!(payment_method_data, PaymentMethodCreateData::Card(_)) } @@ -489,6 +489,7 @@ pub struct CardDetail { pub nick_name: Option>, /// Card Issuing Country + #[schema(value_type = CountryAlpha2)] pub card_issuing_country: Option, /// Card's Network @@ -709,7 +710,7 @@ pub struct PaymentMethodResponse { #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema, Clone)] pub struct PaymentMethodResponse { /// Unique identifier for a merchant - #[schema(example = "merchant_1671528864")] + #[schema(example = "merchant_1671528864", value_type = String)] pub merchant_id: id_type::MerchantId, /// The unique identifier of the customer. @@ -722,11 +723,11 @@ pub struct PaymentMethodResponse { /// The type of payment method use for the payment. #[schema(value_type = PaymentMethod, example = "card")] - pub payment_method: Option, + pub payment_method_type: Option, /// This is a sub-category of payment method. #[schema(value_type = Option, example = "credit")] - pub payment_method_type: Option, + pub payment_method_subtype: Option, /// Indicates whether the payment method is eligible for recurring payments #[schema(example = true)] @@ -852,6 +853,7 @@ pub struct CardDetailFromLocker { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct CardDetailFromLocker { + #[schema(value_type = Option)] pub issuer_country: Option, pub last4_digits: Option, #[serde(skip)] @@ -1099,10 +1101,14 @@ pub struct BankDebitTypes { pub eligible_connectors: Vec, } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] #[derive(Debug, Clone, serde::Serialize, ToSchema, PartialEq)] pub struct ResponsePaymentMethodTypes { /// The payment method type enabled - #[schema(example = "klarna")] + #[schema(example = "klarna", value_type = PaymentMethodType)] pub payment_method_type: api_enums::PaymentMethodType, /// The list of payment experiences enabled, if applicable for a payment method type @@ -1116,6 +1122,39 @@ pub struct ResponsePaymentMethodTypes { /// The Bank debit payment method information, if applicable for a payment method type. pub bank_debits: Option, + + /// The Bank transfer payment method information, if applicable for a payment method type. + pub bank_transfers: Option, + + /// Required fields for the payment_method_type. + pub required_fields: Option>, + + /// surcharge details for this payment method type if exists + pub surcharge_details: Option, + + /// auth service connector label for this payment method type, if exists + pub pm_auth_connector: Option, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, ToSchema, PartialEq)] +pub struct ResponsePaymentMethodTypes { + /// The payment method type enabled + #[schema(example = "klarna", value_type = PaymentMethodType)] + pub payment_method_subtype: api_enums::PaymentMethodType, + + /// The list of payment experiences enabled, if applicable for a payment method type + pub payment_experience: Option>, + + /// The list of card networks enabled, if applicable for a payment method type + pub card_networks: Option>, + + /// The list of banks enabled, if applicable for a payment method type + pub bank_names: Option>, + + /// The Bank debit payment method information, if applicable for a payment method type. + pub bank_debits: Option, + /// The Bank transfer payment method information, if applicable for a payment method type. pub bank_transfers: Option, @@ -1541,18 +1580,6 @@ pub struct PaymentMethodListResponse { pub currency: Option, /// Information about the payment method - #[schema(value_type = Vec,example = json!( - [ - { - "payment_method": "wallet", - "payment_experience": null, - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ] - } - ] - ))] pub payment_methods: Vec, /// Value indicating if the current payment is a mandate payment #[schema(value_type = MandateType)] @@ -1582,35 +1609,6 @@ pub struct PaymentMethodListResponse { pub is_tax_calculation_enabled: bool, } -#[derive(Eq, PartialEq, Hash, Debug, serde::Deserialize, ToSchema)] -pub struct PaymentMethodList { - /// The type of payment method use for the payment. - #[schema(value_type = PaymentMethod,example = "card")] - pub payment_method: api_enums::PaymentMethod, - - /// This is a sub-category of payment method. - #[schema(value_type = Option>,example = json!(["credit"]))] - pub payment_method_types: Option>, -} - -/// Currently if the payment method is Wallet or Paylater the relevant fields are `payment_method` -/// and `payment_method_issuers`. Otherwise only consider -/// `payment_method`,`payment_method_issuers`,`payment_method_types`,`payment_schemes` fields. -impl serde::Serialize for PaymentMethodList { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut state = serializer.serialize_struct("PaymentMethodList", 4)?; - state.serialize_field("payment_method", &self.payment_method)?; - - state.serialize_field("payment_method_types", &self.payment_method_types)?; - - state.end() - } -} - #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") @@ -1686,11 +1684,11 @@ pub struct CustomerPaymentMethod { /// The type of payment method use for the payment. #[schema(value_type = PaymentMethod,example = "card")] - pub payment_method: api_enums::PaymentMethod, + pub payment_method_type: api_enums::PaymentMethod, /// This is a sub-category of payment method. #[schema(value_type = Option,example = "credit_card")] - pub payment_method_type: Option, + pub payment_method_subtype: Option, /// Indicates whether the payment method is eligible for recurring payments #[schema(example = true)] @@ -1734,6 +1732,7 @@ pub struct CustomerPaymentMethod { pub enum PaymentMethodListData { Card(CardDetailFromLocker), #[cfg(feature = "payouts")] + #[schema(value_type = Bank)] Bank(payouts::Bank), } @@ -2145,8 +2144,8 @@ impl From for PaymentMethodMigrationResponse match response { Ok(res) => Self { payment_method_id: Some(res.payment_method_id), - payment_method: res.payment_method, - payment_method_type: res.payment_method_type, + payment_method: res.payment_method_type, + payment_method_type: res.payment_method_subtype, customer_id: Some(res.customer_id), migration_status: MigrationStatus::Success, migration_error: None, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 2f7f796aef1..98bc7b754a4 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -63,6 +63,7 @@ pub struct ConnectorCode { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, PartialEq, Eq)] pub struct BankCodeResponse { + #[schema(value_type = Vec)] pub bank_name: Vec, pub eligible_connectors: Vec, } diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 9ca88267ae6..d607cd04bff 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -74,8 +74,6 @@ pub struct PaymentMethod { pub merchant_id: common_utils::id_type::MerchantId, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, - pub payment_method: Option, - pub payment_method_type: Option, pub payment_method_data: Option, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, @@ -87,6 +85,8 @@ pub struct PaymentMethod { pub payment_method_billing_address: Option, pub updated_by: Option, pub locker_fingerprint_id: Option, + pub payment_method_type_v2: Option, + pub payment_method_subtype: Option, pub id: common_utils::id_type::GlobalPaymentMethodId, pub version: common_enums::ApiVersion, pub network_token_requestor_reference_id: Option, @@ -160,8 +160,6 @@ pub struct PaymentMethodNew { pub struct PaymentMethodNew { pub customer_id: common_utils::id_type::CustomerId, pub merchant_id: common_utils::id_type::MerchantId, - pub payment_method: Option, - pub payment_method_type: Option, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, pub payment_method_data: Option, @@ -175,6 +173,8 @@ pub struct PaymentMethodNew { pub payment_method_billing_address: Option, pub updated_by: Option, pub locker_fingerprint_id: Option, + pub payment_method_type_v2: Option, + pub payment_method_subtype: Option, pub id: common_utils::id_type::GlobalPaymentMethodId, pub version: common_enums::ApiVersion, pub network_token_requestor_reference_id: Option, @@ -274,8 +274,8 @@ pub enum PaymentMethodUpdate { payment_method_data: Option, status: Option, locker_id: Option, - payment_method: Option, - payment_method_type: Option, + payment_method_type_v2: Option, + payment_method_subtype: Option, network_token_requestor_reference_id: Option, network_token_locker_id: Option, network_token_payment_method_data: Option, @@ -305,10 +305,10 @@ pub struct PaymentMethodUpdateInternal { network_transaction_id: Option, status: Option, locker_id: Option, - payment_method: Option, + payment_method_type_v2: Option, connector_mandate_details: Option, updated_by: Option, - payment_method_type: Option, + payment_method_subtype: Option, last_modified: PrimitiveDateTime, network_token_requestor_reference_id: Option, network_token_locker_id: Option, @@ -324,10 +324,10 @@ impl PaymentMethodUpdateInternal { network_transaction_id, status, locker_id, - payment_method, + payment_method_type_v2, connector_mandate_details, updated_by, - payment_method_type, + payment_method_subtype, last_modified, network_token_requestor_reference_id, network_token_locker_id, @@ -339,8 +339,6 @@ impl PaymentMethodUpdateInternal { merchant_id: source.merchant_id, created_at: source.created_at, last_modified, - payment_method: payment_method.or(source.payment_method), - payment_method_type: payment_method_type.or(source.payment_method_type), payment_method_data: payment_method_data.or(source.payment_method_data), locker_id: locker_id.or(source.locker_id), last_used_at: last_used_at.unwrap_or(source.last_used_at), @@ -353,6 +351,8 @@ impl PaymentMethodUpdateInternal { payment_method_billing_address: source.payment_method_billing_address, updated_by: updated_by.or(source.updated_by), locker_fingerprint_id: source.locker_fingerprint_id, + payment_method_type_v2: payment_method_type_v2.or(source.payment_method_type_v2), + payment_method_subtype: payment_method_subtype.or(source.payment_method_subtype), id: source.id, version: source.version, network_token_requestor_reference_id: network_token_requestor_reference_id @@ -636,10 +636,10 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status: None, locker_id: None, - payment_method: None, + payment_method_type_v2: None, connector_mandate_details: None, updated_by: None, - payment_method_type: None, + payment_method_subtype: None, last_modified: common_utils::date_time::now(), network_token_locker_id: None, network_token_requestor_reference_id: None, @@ -651,10 +651,10 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status: None, locker_id: None, - payment_method: None, + payment_method_type_v2: None, connector_mandate_details: None, updated_by: None, - payment_method_type: None, + payment_method_subtype: None, last_modified: common_utils::date_time::now(), network_token_locker_id: None, network_token_requestor_reference_id: None, @@ -669,10 +669,10 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status: None, locker_id: None, - payment_method: None, + payment_method_type_v2: None, connector_mandate_details: None, updated_by: None, - payment_method_type: None, + payment_method_subtype: None, last_modified: common_utils::date_time::now(), network_token_locker_id: None, network_token_requestor_reference_id: None, @@ -687,10 +687,10 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id, status, locker_id: None, - payment_method: None, + payment_method_type_v2: None, connector_mandate_details: None, updated_by: None, - payment_method_type: None, + payment_method_subtype: None, last_modified: common_utils::date_time::now(), network_token_locker_id: None, network_token_requestor_reference_id: None, @@ -702,10 +702,10 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status, locker_id: None, - payment_method: None, + payment_method_type_v2: None, connector_mandate_details: None, updated_by: None, - payment_method_type: None, + payment_method_subtype: None, last_modified: common_utils::date_time::now(), network_token_locker_id: None, network_token_requestor_reference_id: None, @@ -715,8 +715,8 @@ impl From for PaymentMethodUpdateInternal { payment_method_data, status, locker_id, - payment_method, - payment_method_type, + payment_method_type_v2, + payment_method_subtype, network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, @@ -726,10 +726,10 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status, locker_id, - payment_method, + payment_method_type_v2, connector_mandate_details: None, updated_by: None, - payment_method_type, + payment_method_subtype, last_modified: common_utils::date_time::now(), network_token_requestor_reference_id, network_token_locker_id, @@ -742,11 +742,11 @@ impl From for PaymentMethodUpdateInternal { last_used_at: None, status: None, locker_id: None, - payment_method: None, + payment_method_type_v2: None, connector_mandate_details, network_transaction_id: None, updated_by: None, - payment_method_type: None, + payment_method_subtype: None, last_modified: common_utils::date_time::now(), network_token_locker_id: None, network_token_requestor_reference_id: None, @@ -816,8 +816,6 @@ impl From<&PaymentMethodNew> for PaymentMethod { locker_id: payment_method_new.locker_id.clone(), created_at: payment_method_new.created_at, last_modified: payment_method_new.last_modified, - payment_method: payment_method_new.payment_method, - payment_method_type: payment_method_new.payment_method_type, payment_method_data: payment_method_new.payment_method_data.clone(), last_used_at: payment_method_new.last_used_at, connector_mandate_details: payment_method_new.connector_mandate_details.clone(), @@ -829,8 +827,10 @@ impl From<&PaymentMethodNew> for PaymentMethod { payment_method_billing_address: payment_method_new .payment_method_billing_address .clone(), - id: payment_method_new.id.clone(), locker_fingerprint_id: payment_method_new.locker_fingerprint_id.clone(), + payment_method_type_v2: payment_method_new.payment_method_type_v2, + payment_method_subtype: payment_method_new.payment_method_subtype, + id: payment_method_new.id.clone(), version: payment_method_new.version, network_token_requestor_reference_id: payment_method_new .network_token_requestor_reference_id @@ -843,6 +843,10 @@ impl From<&PaymentMethodNew> for PaymentMethod { } } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentsMandateReferenceRecord { pub connector_mandate_id: String, @@ -854,6 +858,18 @@ pub struct PaymentsMandateReferenceRecord { pub connector_mandate_request_reference_id: Option, } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReferenceRecord { + pub connector_mandate_id: String, + pub payment_method_subtype: Option, + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, + pub mandate_metadata: Option, + pub connector_mandate_status: Option, + pub connector_mandate_request_reference_id: Option, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] #[diesel(sql_type = diesel::sql_types::Jsonb)] pub struct PaymentsMandateReference( diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index e3097f80db9..aa7f3f7232c 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -938,9 +938,6 @@ diesel::table! { merchant_id -> Varchar, created_at -> Timestamp, last_modified -> Timestamp, - payment_method -> Nullable, - #[max_length = 64] - payment_method_type -> Nullable, payment_method_data -> Nullable, #[max_length = 64] locker_id -> Nullable, @@ -959,6 +956,10 @@ diesel::table! { #[max_length = 64] locker_fingerprint_id -> Nullable, #[max_length = 64] + payment_method_type_v2 -> Nullable, + #[max_length = 64] + payment_method_subtype -> Nullable, + #[max_length = 64] id -> Varchar, version -> ApiVersion, #[max_length = 128] diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index 4ff246d3760..083d6e47501 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -1,10 +1,9 @@ +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use common_utils::crypto::Encryptable; use common_utils::{ crypto::OptionalEncryptableValue, - // date_time, - // encryption::Encryption, errors::{CustomResult, ValidationError}, - pii, - type_name, + pii, type_name, types::keymanager, }; use diesel_models::enums as storage_enums; @@ -12,6 +11,8 @@ use error_stack::ResultExt; use masking::{PeekInterface, Secret}; use time::PrimitiveDateTime; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::type_encryption::EncryptedJsonType; use crate::type_encryption::{crypto_operation, AsyncLift, CryptoOperation}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -77,9 +78,11 @@ pub struct PaymentMethod { pub merchant_id: common_utils::id_type::MerchantId, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, - pub payment_method: Option, - pub payment_method_type: Option, - pub payment_method_data: OptionalEncryptableValue, + pub payment_method_type: Option, + pub payment_method_subtype: Option, + pub payment_method_data: Option< + Encryptable>>, + >, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, pub connector_mandate_details: Option, @@ -110,6 +113,32 @@ impl PaymentMethod { pub fn get_id(&self) -> &common_utils::id_type::GlobalPaymentMethodId { &self.id } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + pub fn get_payment_method_type(&self) -> Option { + self.payment_method + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + pub fn get_payment_method_type(&self) -> Option { + self.payment_method_type + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + pub fn get_payment_method_subtype(&self) -> Option { + self.payment_method_type + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + pub fn get_payment_method_subtype(&self) -> Option { + self.payment_method_subtype + } } #[cfg(all( @@ -311,8 +340,8 @@ impl super::behaviour::Conversion for PaymentMethod { id: self.id, created_at: self.created_at, last_modified: self.last_modified, - payment_method: self.payment_method, - payment_method_type: self.payment_method_type, + payment_method_type_v2: self.payment_method_type, + payment_method_subtype: self.payment_method_subtype, payment_method_data: self.payment_method_data.map(|val| val.into()), locker_id: self.locker_id.map(|id| id.get_string_repr().clone()), last_used_at: self.last_used_at, @@ -351,8 +380,8 @@ impl super::behaviour::Conversion for PaymentMethod { id: item.id, created_at: item.created_at, last_modified: item.last_modified, - payment_method: item.payment_method, - payment_method_type: item.payment_method_type, + payment_method_type: item.payment_method_type_v2, + payment_method_subtype: item.payment_method_subtype, payment_method_data: item .payment_method_data .async_lift(|inner| async { @@ -422,8 +451,8 @@ impl super::behaviour::Conversion for PaymentMethod { id: self.id, created_at: self.created_at, last_modified: self.last_modified, - payment_method: self.payment_method, - payment_method_type: self.payment_method_type, + payment_method_type_v2: self.payment_method_type, + payment_method_subtype: self.payment_method_subtype, payment_method_data: self.payment_method_data.map(|val| val.into()), locker_id: self.locker_id.map(|id| id.get_string_repr().clone()), last_used_at: self.last_used_at, diff --git a/crates/hyperswitch_domain_models/src/type_encryption.rs b/crates/hyperswitch_domain_models/src/type_encryption.rs index 983528dee98..641e295155f 100644 --- a/crates/hyperswitch_domain_models/src/type_encryption.rs +++ b/crates/hyperswitch_domain_models/src/type_encryption.rs @@ -18,6 +18,7 @@ mod encrypt { crypto, encryption::Encryption, errors::{self, CustomResult}, + ext_traits::ByteSliceExt, keymanager::call_encryption_service, transformers::{ForeignFrom, ForeignTryFrom}, types::keymanager::{ @@ -32,7 +33,7 @@ mod encrypt { use router_env::{instrument, logger, tracing}; use rustc_hash::FxHashMap; - use super::metrics; + use super::{metrics, EncryptedJsonType}; #[async_trait] pub trait TypeEncryption< @@ -115,8 +116,8 @@ mod encrypt { S: masking::Strategy + Send + Sync, > TypeEncryption for crypto::Encryptable> { + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] - #[allow(unused_variables)] async fn encrypt_via_api( state: &KeyManagerState, masked_data: Secret, @@ -150,8 +151,8 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] - #[allow(unused_variables)] async fn decrypt_via_api( state: &KeyManagerState, encrypted_data: Encryption, @@ -194,6 +195,8 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn encrypt( masked_data: Secret, key: &[u8], @@ -204,6 +207,8 @@ mod encrypt { Ok(Self::new(masked_data, encrypted_data.into())) } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn decrypt( encrypted_data: Encryption, key: &[u8], @@ -220,7 +225,8 @@ mod encrypt { Ok(Self::new(value.into(), encrypted)) } - #[allow(unused_variables)] + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_encrypt_via_api( state: &KeyManagerState, masked_data: FxHashMap>, @@ -254,7 +260,8 @@ mod encrypt { } } - #[allow(unused_variables)] + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_decrypt_via_api( state: &KeyManagerState, encrypted_data: FxHashMap, @@ -296,6 +303,8 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_encrypt( masked_data: FxHashMap>, key: &[u8], @@ -316,6 +325,8 @@ mod encrypt { .collect() } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_decrypt( encrypted_data: FxHashMap, key: &[u8], @@ -342,8 +353,8 @@ mod encrypt { > TypeEncryption for crypto::Encryptable> { + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] - #[allow(unused_variables)] async fn encrypt_via_api( state: &KeyManagerState, masked_data: Secret, @@ -377,8 +388,8 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] - #[allow(unused_variables)] async fn decrypt_via_api( state: &KeyManagerState, encrypted_data: Encryption, @@ -420,6 +431,7 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] async fn encrypt( masked_data: Secret, @@ -433,6 +445,7 @@ mod encrypt { Ok(Self::new(masked_data, encrypted_data.into())) } + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] async fn decrypt( encrypted_data: Encryption, @@ -448,7 +461,8 @@ mod encrypt { Ok(Self::new(value.into(), encrypted)) } - #[allow(unused_variables)] + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_encrypt_via_api( state: &KeyManagerState, masked_data: FxHashMap>, @@ -482,7 +496,8 @@ mod encrypt { } } - #[allow(unused_variables)] + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_decrypt_via_api( state: &KeyManagerState, encrypted_data: FxHashMap, @@ -524,6 +539,8 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_encrypt( masked_data: FxHashMap>, key: &[u8], @@ -543,6 +560,8 @@ mod encrypt { .collect() } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_decrypt( encrypted_data: FxHashMap, key: &[u8], @@ -562,14 +581,251 @@ mod encrypt { } } + impl EncryptedJsonType + where + T: std::fmt::Debug + Clone + serde::Serialize + serde::de::DeserializeOwned, + { + fn serialize_json_bytes(&self) -> CustomResult>, errors::CryptoError> { + common_utils::ext_traits::Encode::encode_to_vec(self.inner()) + .change_context(errors::CryptoError::EncodingFailed) + .attach_printable("Failed to JSON serialize data before encryption") + .map(Secret::new) + } + + fn deserialize_json_bytes( + bytes: Secret>, + ) -> CustomResult, errors::ParsingError> + where + S: masking::Strategy, + { + bytes + .peek() + .as_slice() + .parse_struct::(std::any::type_name::()) + .map(|result| Secret::new(Self::from(result))) + .attach_printable("Failed to JSON deserialize data after decryption") + } + } + + #[async_trait] + impl< + T: std::fmt::Debug + Clone + serde::Serialize + serde::de::DeserializeOwned + Send, + V: crypto::DecodeMessage + crypto::EncodeMessage + Send + 'static, + S: masking::Strategy> + Send + Sync, + > TypeEncryption, V, S> + for crypto::Encryptable, S>> + { + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn encrypt_via_api( + state: &KeyManagerState, + masked_data: Secret, S>, + identifier: Identifier, + key: &[u8], + crypt_algo: V, + ) -> CustomResult { + let data_bytes = EncryptedJsonType::serialize_json_bytes(masked_data.peek())?; + let result: crypto::Encryptable>> = + TypeEncryption::encrypt_via_api(state, data_bytes, identifier, key, crypt_algo) + .await?; + Ok(Self::new(masked_data, result.into_encrypted())) + } + + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn decrypt_via_api( + state: &KeyManagerState, + encrypted_data: Encryption, + identifier: Identifier, + key: &[u8], + crypt_algo: V, + ) -> CustomResult { + let result: crypto::Encryptable>> = + TypeEncryption::decrypt_via_api(state, encrypted_data, identifier, key, crypt_algo) + .await?; + result + .deserialize_inner_value(EncryptedJsonType::deserialize_json_bytes) + .change_context(errors::CryptoError::DecodingFailed) + } + + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn encrypt( + masked_data: Secret, S>, + key: &[u8], + crypt_algo: V, + ) -> CustomResult { + let data_bytes = EncryptedJsonType::serialize_json_bytes(masked_data.peek())?; + let result: crypto::Encryptable>> = + TypeEncryption::encrypt(data_bytes, key, crypt_algo).await?; + Ok(Self::new(masked_data, result.into_encrypted())) + } + + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn decrypt( + encrypted_data: Encryption, + key: &[u8], + crypt_algo: V, + ) -> CustomResult { + let result: crypto::Encryptable>> = + TypeEncryption::decrypt(encrypted_data, key, crypt_algo).await?; + result + .deserialize_inner_value(EncryptedJsonType::deserialize_json_bytes) + .change_context(errors::CryptoError::DecodingFailed) + } + + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn batch_encrypt_via_api( + state: &KeyManagerState, + masked_data: FxHashMap, S>>, + identifier: Identifier, + key: &[u8], + crypt_algo: V, + ) -> CustomResult, errors::CryptoError> { + let hashmap_capacity = masked_data.len(); + let data_bytes = masked_data.iter().try_fold( + FxHashMap::with_capacity_and_hasher(hashmap_capacity, Default::default()), + |mut map, (key, value)| { + let value_bytes = EncryptedJsonType::serialize_json_bytes(value.peek())?; + map.insert(key.to_owned(), value_bytes); + Ok::<_, error_stack::Report>(map) + }, + )?; + + let result: FxHashMap>>> = + TypeEncryption::batch_encrypt_via_api( + state, data_bytes, identifier, key, crypt_algo, + ) + .await?; + let result_hashmap = result.into_iter().try_fold( + FxHashMap::with_capacity_and_hasher(hashmap_capacity, Default::default()), + |mut map, (key, value)| { + let original_value = masked_data + .get(&key) + .ok_or(errors::CryptoError::EncodingFailed) + .attach_printable_lazy(|| { + format!("Failed to find {key} in input hashmap") + })?; + map.insert( + key, + Self::new(original_value.clone(), value.into_encrypted()), + ); + Ok::<_, error_stack::Report>(map) + }, + )?; + + Ok(result_hashmap) + } + + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn batch_decrypt_via_api( + state: &KeyManagerState, + encrypted_data: FxHashMap, + identifier: Identifier, + key: &[u8], + crypt_algo: V, + ) -> CustomResult, errors::CryptoError> { + let result: FxHashMap>>> = + TypeEncryption::batch_decrypt_via_api( + state, + encrypted_data, + identifier, + key, + crypt_algo, + ) + .await?; + + let hashmap_capacity = result.len(); + let result_hashmap = result.into_iter().try_fold( + FxHashMap::with_capacity_and_hasher(hashmap_capacity, Default::default()), + |mut map, (key, value)| { + let deserialized_value = value + .deserialize_inner_value(EncryptedJsonType::deserialize_json_bytes) + .change_context(errors::CryptoError::DecodingFailed)?; + map.insert(key, deserialized_value); + Ok::<_, error_stack::Report>(map) + }, + )?; + + Ok(result_hashmap) + } + + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn batch_encrypt( + masked_data: FxHashMap, S>>, + key: &[u8], + crypt_algo: V, + ) -> CustomResult, errors::CryptoError> { + let hashmap_capacity = masked_data.len(); + let data_bytes = masked_data.iter().try_fold( + FxHashMap::with_capacity_and_hasher(hashmap_capacity, Default::default()), + |mut map, (key, value)| { + let value_bytes = EncryptedJsonType::serialize_json_bytes(value.peek())?; + map.insert(key.to_owned(), value_bytes); + Ok::<_, error_stack::Report>(map) + }, + )?; + + let result: FxHashMap>>> = + TypeEncryption::batch_encrypt(data_bytes, key, crypt_algo).await?; + let result_hashmap = result.into_iter().try_fold( + FxHashMap::with_capacity_and_hasher(hashmap_capacity, Default::default()), + |mut map, (key, value)| { + let original_value = masked_data + .get(&key) + .ok_or(errors::CryptoError::EncodingFailed) + .attach_printable_lazy(|| { + format!("Failed to find {key} in input hashmap") + })?; + map.insert( + key, + Self::new(original_value.clone(), value.into_encrypted()), + ); + Ok::<_, error_stack::Report>(map) + }, + )?; + + Ok(result_hashmap) + } + + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] + async fn batch_decrypt( + encrypted_data: FxHashMap, + key: &[u8], + crypt_algo: V, + ) -> CustomResult, errors::CryptoError> { + let result: FxHashMap>>> = + TypeEncryption::batch_decrypt(encrypted_data, key, crypt_algo).await?; + + let hashmap_capacity = result.len(); + let result_hashmap = result.into_iter().try_fold( + FxHashMap::with_capacity_and_hasher(hashmap_capacity, Default::default()), + |mut map, (key, value)| { + let deserialized_value = value + .deserialize_inner_value(EncryptedJsonType::deserialize_json_bytes) + .change_context(errors::CryptoError::DecodingFailed)?; + map.insert(key, deserialized_value); + Ok::<_, error_stack::Report>(map) + }, + )?; + + Ok(result_hashmap) + } + } + #[async_trait] impl< V: crypto::DecodeMessage + crypto::EncodeMessage + Send + 'static, S: masking::Strategy> + Send + Sync, > TypeEncryption, V, S> for crypto::Encryptable, S>> { + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] - #[allow(unused_variables)] async fn encrypt_via_api( state: &KeyManagerState, masked_data: Secret, S>, @@ -603,8 +859,8 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] - #[allow(unused_variables)] async fn decrypt_via_api( state: &KeyManagerState, encrypted_data: Encryption, @@ -646,6 +902,7 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] async fn encrypt( masked_data: Secret, S>, @@ -657,6 +914,7 @@ mod encrypt { Ok(Self::new(masked_data, encrypted_data.into())) } + // Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all)] async fn decrypt( encrypted_data: Encryption, @@ -669,7 +927,8 @@ mod encrypt { Ok(Self::new(data.into(), encrypted)) } - #[allow(unused_variables)] + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_encrypt_via_api( state: &KeyManagerState, masked_data: FxHashMap, S>>, @@ -703,7 +962,8 @@ mod encrypt { } } - #[allow(unused_variables)] + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_decrypt_via_api( state: &KeyManagerState, encrypted_data: FxHashMap, @@ -745,6 +1005,8 @@ mod encrypt { } } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_encrypt( masked_data: FxHashMap, S>>, key: &[u8], @@ -762,6 +1024,8 @@ mod encrypt { .collect() } + // Do not remove the `skip_all` as the key would be logged otherwise + #[instrument(skip_all)] async fn batch_decrypt( encrypted_data: FxHashMap, key: &[u8], @@ -785,6 +1049,37 @@ mod encrypt { } } } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct EncryptedJsonType(T); + +impl EncryptedJsonType { + pub fn inner(&self) -> &T { + &self.0 + } + + pub fn into_inner(self) -> T { + self.0 + } +} + +impl From for EncryptedJsonType { + fn from(value: T) -> Self { + Self(value) + } +} + +impl std::ops::Deref for EncryptedJsonType { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner() + } +} + +/// Type alias for `Option>>>` +pub type OptionalEncryptableJsonType = Option>>>; + pub trait Lift { type SelfWrapper; type OtherWrapper; @@ -983,6 +1278,7 @@ pub enum CryptoOutput> { BatchOperation(FxHashMap>>), } +// Do not remove the `skip_all` as the key would be logged otherwise #[instrument(skip_all, fields(table = table_name))] pub async fn crypto_operation>( state: &KeyManagerState, diff --git a/crates/openapi/Cargo.toml b/crates/openapi/Cargo.toml index 26707479b0d..930f9739e6e 100644 --- a/crates/openapi/Cargo.toml +++ b/crates/openapi/Cargo.toml @@ -16,7 +16,7 @@ common_utils = { version = "0.1.0", path = "../common_utils", features = ["logs" router_env = { version = "0.1.0", path = "../router_env" } [features] -v2 = ["api_models/v2", "api_models/customer_v2", "common_utils/v2"] +v2 = ["api_models/v2", "api_models/customer_v2", "common_utils/v2", "api_models/payment_methods_v2", "common_utils/payment_methods_v2"] v1 = ["api_models/v1", "common_utils/v1"] [lints] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index dc9274b71ee..6356b5f15be 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -231,9 +231,14 @@ Never share your secret api keys. Keep them guarded and secure. api_models::customers::CustomerDeleteResponse, api_models::payment_methods::PaymentMethodCreate, api_models::payment_methods::PaymentMethodResponse, - api_models::payment_methods::PaymentMethodList, api_models::payment_methods::CustomerPaymentMethod, api_models::payment_methods::PaymentMethodListResponse, + api_models::payment_methods::ResponsePaymentMethodsEnabled, + api_models::payment_methods::ResponsePaymentMethodTypes, + api_models::payment_methods::PaymentExperienceTypes, + api_models::payment_methods::CardNetworkTypes, + api_models::payment_methods::BankDebitTypes, + api_models::payment_methods::BankTransferTypes, api_models::payment_methods::CustomerPaymentMethodsListResponse, api_models::payment_methods::PaymentMethodDeleteResponse, api_models::payment_methods::PaymentMethodUpdate, @@ -507,6 +512,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::MobilePaymentData, api_models::payments::MobilePaymentResponse, api_models::payments::Address, + api_models::payments::BankCodeResponse, api_models::payouts::CardPayout, api_models::payouts::Wallet, api_models::payouts::Paypal, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 7417466bfd2..17c00416933 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -171,18 +171,27 @@ Never share your secret api keys. Keep them guarded and secure. api_models::customers::CustomerDeleteResponse, api_models::payment_methods::PaymentMethodCreate, api_models::payment_methods::PaymentMethodResponse, - api_models::payment_methods::PaymentMethodList, + api_models::payment_methods::PaymentMethodResponseData, api_models::payment_methods::CustomerPaymentMethod, api_models::payment_methods::PaymentMethodListResponse, + api_models::payment_methods::ResponsePaymentMethodsEnabled, + api_models::payment_methods::ResponsePaymentMethodTypes, + api_models::payment_methods::PaymentExperienceTypes, + api_models::payment_methods::CardNetworkTypes, + api_models::payment_methods::BankDebitTypes, + api_models::payment_methods::BankTransferTypes, api_models::payment_methods::CustomerPaymentMethodsListResponse, api_models::payment_methods::PaymentMethodDeleteResponse, api_models::payment_methods::PaymentMethodUpdate, + api_models::payment_methods::PaymentMethodUpdateData, api_models::payment_methods::CustomerDefaultPaymentMethodResponse, api_models::payment_methods::CardDetailFromLocker, api_models::payment_methods::PaymentMethodCreateData, api_models::payment_methods::CardDetail, api_models::payment_methods::CardDetailUpdate, api_models::payment_methods::RequestPaymentMethodTypes, + api_models::payment_methods::CardType, + api_models::payment_methods::PaymentMethodListData, api_models::poll::PollResponse, api_models::poll::PollStatus, api_models::customers::CustomerResponse, @@ -426,6 +435,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentsConfirmIntentRequest, api_models::payments::PaymentsConfirmIntentResponse, api_models::payments::AmountDetailsResponse, + api_models::payments::BankCodeResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::DefaultPaymentMethod, api_models::payment_methods::MaskedBankDetails, diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 6d47ac174f3..266873b1a11 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -621,7 +621,7 @@ impl CustomerDeleteBridge for customers::GlobalId { // check this in review Ok(customer_payment_methods) => { for pm in customer_payment_methods.into_iter() { - if pm.payment_method == Some(enums::PaymentMethod::Card) { + if pm.payment_method_type_v2 == Some(enums::PaymentMethod::Card) { cards::delete_card_by_locker_id(state, &self.id, merchant_account.get_id()) .await .switch()?; @@ -796,7 +796,7 @@ impl CustomerDeleteBridge for customers::CustomerId { // check this in review Ok(customer_payment_methods) => { for pm in customer_payment_methods.into_iter() { - if pm.payment_method == Some(enums::PaymentMethod::Card) { + if pm.get_payment_method_type() == Some(enums::PaymentMethod::Card) { cards::delete_card_from_locker( state, &self.customer_id, diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index 8fd4b10ed3f..dbadadb3a6f 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -138,10 +138,12 @@ pub async fn call_to_locker( ) -> CustomResult { let mut cards_moved = 0; - for pm in payment_methods - .into_iter() - .filter(|pm| matches!(pm.payment_method, Some(storage_enums::PaymentMethod::Card))) - { + for pm in payment_methods.into_iter().filter(|pm| { + matches!( + pm.get_payment_method_type(), + Some(storage_enums::PaymentMethod::Card) + ) + }) { let card = cards::get_card_from_locker( state, customer_id, @@ -171,8 +173,8 @@ pub async fn call_to_locker( }; let pm_create = api::PaymentMethodCreate { - payment_method: pm.payment_method, - payment_method_type: pm.payment_method_type, + payment_method: pm.get_payment_method_type(), + payment_method_type: pm.get_payment_method_subtype(), payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, card: Some(card_details.clone()), diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 932a4a26ee0..8b8b3f33ed8 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -673,14 +673,14 @@ pub async fn retrieve_payment_method_with_token( #[instrument(skip_all)] pub(crate) async fn get_payment_method_create_request( payment_method_data: Option<&domain::PaymentMethodData>, - payment_method: Option, - payment_method_type: Option, + payment_method_type: Option, + payment_method_subtype: Option, customer_id: &Option, billing_name: Option>, ) -> RouterResult { match payment_method_data { - Some(pm_data) => match payment_method { - Some(payment_method) => match pm_data { + Some(pm_data) => match payment_method_type { + Some(payment_method_type) => match pm_data { domain::PaymentMethodData::Card(card) => { let card_detail = payment_methods::CardDetail { card_number: card.card_number.clone(), @@ -706,9 +706,9 @@ pub(crate) async fn get_payment_method_create_request( .flatten(), }; let payment_method_request = payment_methods::PaymentMethodCreate { - payment_method, - payment_method_type: payment_method_type - .get_required_value("Payment_method_type") + payment_method_type, + payment_method_subtype: payment_method_subtype + .get_required_value("payment_method_subtype") .change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_data", })?, @@ -900,8 +900,8 @@ pub async fn create_payment_method( state, key_store, Some(resp.vault_id.get_string_repr().clone()), - Some(req.payment_method), Some(req.payment_method_type), + Some(req.payment_method_subtype), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1072,8 +1072,8 @@ pub async fn payment_method_intent_confirm( state, key_store, Some(resp.vault_id.get_string_repr().clone()), - Some(req.payment_method), Some(req.payment_method_type), + Some(req.payment_method_subtype), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1132,7 +1132,9 @@ pub async fn create_payment_method_in_db( locker_id: Option, merchant_id: &id_type::MerchantId, customer_acceptance: Option, - payment_method_data: crypto::OptionalEncryptableValue, + payment_method_data: domain::types::OptionalEncryptableJsonType< + api::payment_methods::PaymentMethodsData, + >, key_store: &domain::MerchantKeyStore, connector_mandate_details: Option, status: Option, @@ -1154,8 +1156,8 @@ pub async fn create_payment_method_in_db( merchant_id: merchant_id.to_owned(), id: payment_method_id, locker_id, - payment_method: Some(req.payment_method), payment_method_type: Some(req.payment_method_type), + payment_method_subtype: Some(req.payment_method_subtype), payment_method_data, connector_mandate_details, customer_acceptance, @@ -1208,8 +1210,8 @@ pub async fn create_payment_method_for_intent( merchant_id: merchant_id.to_owned(), id: payment_method_id, locker_id: None, - payment_method: None, payment_method_type: None, + payment_method_subtype: None, payment_method_data: None, connector_mandate_details: None, customer_acceptance: None, @@ -1242,8 +1244,8 @@ pub async fn create_pm_additional_data_update( state: &SessionState, key_store: &domain::MerchantKeyStore, vault_id: Option, - payment_method: Option, - payment_method_type: Option, + payment_method_type: Option, + payment_method_subtype: Option, ) -> RouterResult { let card = match pmd { pm_types::PaymentMethodVaultingData::Card(card) => { @@ -1260,8 +1262,8 @@ pub async fn create_pm_additional_data_update( let pm_update = storage::PaymentMethodUpdate::AdditionalDataUpdate { status: Some(enums::PaymentMethodStatus::Active), locker_id: vault_id, - payment_method, - payment_method_type, + payment_method_type_v2: payment_method_type, + payment_method_subtype, payment_method_data: Some(pmd.into()), network_token_requestor_reference_id: None, network_token_locker_id: None, @@ -1508,7 +1510,9 @@ pub async fn list_customer_payment_method( let mut filtered_saved_payment_methods_ctx = Vec::new(); for pm in saved_payment_methods.into_iter() { - let payment_method = pm.payment_method.get_required_value("payment_method")?; + let payment_method = pm + .get_payment_method_type() + .get_required_value("payment_method")?; let parent_payment_method_token = is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token")); @@ -1601,9 +1605,11 @@ async fn generate_saved_pm_response( customer: &domain::Customer, payment_info: Option<&pm_types::SavedPMLPaymentsInfo>, ) -> Result> { - let payment_method = pm.payment_method.get_required_value("payment_method")?; + let payment_method_type = pm + .get_payment_method_type() + .get_required_value("payment_method_type")?; - let bank_details = if payment_method == enums::PaymentMethod::BankDebit { + let bank_details = if payment_method_type == enums::PaymentMethod::BankDebit { cards::get_masked_bank_details(&pm) .await .unwrap_or_else(|err| { @@ -1670,8 +1676,8 @@ async fn generate_saved_pm_response( payment_token: parent_payment_method_token.clone(), payment_method_id: pm.get_id().get_string_repr().to_owned(), customer_id: pm.customer_id.to_owned(), - payment_method, - payment_method_type: pm.payment_method_type, + payment_method_type, + payment_method_subtype: pm.get_payment_method_subtype(), payment_method_data: pmd, recurring_enabled: mca_enabled, created: Some(pm.created_at), @@ -1723,8 +1729,7 @@ pub async fn retrieve_payment_method( let pmd = payment_method .payment_method_data .clone() - .map(|x| x.into_inner().expose()) - .and_then(|v| serde_json::from_value::(v).ok()) + .map(|x| x.into_inner().expose().into_inner()) .and_then(|pmd| match pmd { api::PaymentMethodsData::Card(card) => { Some(api::PaymentMethodResponseData::Card(card.into())) @@ -1736,8 +1741,8 @@ pub async fn retrieve_payment_method( merchant_id: payment_method.merchant_id.to_owned(), customer_id: payment_method.customer_id.to_owned(), payment_method_id: payment_method.id.get_string_repr().to_string(), - payment_method: payment_method.payment_method, - payment_method_type: payment_method.payment_method_type, + payment_method_type: payment_method.get_payment_method_type(), + payment_method_subtype: payment_method.get_payment_method_subtype(), created: Some(payment_method.created_at), recurring_enabled: false, last_used_at: Some(payment_method.last_used_at), @@ -1813,8 +1818,8 @@ pub async fn update_payment_method( &state, &key_store, Some(vaulting_response.vault_id.get_string_repr().clone()), - payment_method.payment_method, - payment_method.payment_method_type, + payment_method.get_payment_method_type(), + payment_method.get_payment_method_subtype(), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1956,7 +1961,7 @@ impl pm_types::SavedPMLPaymentsInfo { .get_order_fulfillment_time() .unwrap_or(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME); - pm_routes::ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method)) + pm_routes::ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method_type)) .insert(intent_fulfillment_time, hyperswitch_token_data, state) .await?; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index fcf0c6ca4d1..49a75735ea4 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1915,8 +1915,8 @@ pub async fn update_customer_payment_method( // Construct new payment method object from request let new_pm = api::PaymentMethodCreate { - payment_method: pm.payment_method, - payment_method_type: pm.payment_method_type, + payment_method: pm.get_payment_method_type(), + payment_method_type: pm.get_payment_method_subtype(), payment_method_issuer: pm.payment_method_issuer.clone(), payment_method_issuer_code: pm.payment_method_issuer_code, #[cfg(feature = "payouts")] @@ -2017,10 +2017,10 @@ pub async fn update_customer_payment_method( // Return existing payment method data as response without any changes api::PaymentMethodResponse { merchant_id: pm.merchant_id.to_owned(), - customer_id: Some(pm.customer_id), - payment_method_id: pm.payment_method_id, - payment_method: pm.payment_method, - payment_method_type: pm.payment_method_type, + customer_id: Some(pm.customer_id.clone()), + payment_method_id: pm.payment_method_id.clone(), + payment_method: pm.get_payment_method_type(), + payment_method_type: pm.get_payment_method_subtype(), #[cfg(feature = "payouts")] bank_transfer: None, card: Some(existing_card_data), @@ -3264,14 +3264,14 @@ pub async fn list_payment_methods( let customer_wallet_pm = customer_payment_methods .iter() .filter(|cust_pm| { - cust_pm.payment_method == Some(enums::PaymentMethod::Wallet) + cust_pm.get_payment_method_type() == Some(enums::PaymentMethod::Wallet) }) .collect::>(); response.retain(|mca| { !(mca.payment_method == enums::PaymentMethod::Wallet && customer_wallet_pm.iter().any(|cust_pm| { - cust_pm.payment_method_type == Some(mca.payment_method_type) + cust_pm.get_payment_method_subtype() == Some(mca.payment_method_type) })) }); @@ -4665,7 +4665,9 @@ pub async fn list_customer_payment_method( for pm in resp.into_iter() { let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); - let payment_method = pm.payment_method.get_required_value("payment_method")?; + let payment_method = pm + .get_payment_method_type() + .get_required_value("payment_method")?; let pm_list_context = get_pm_list_context( state, @@ -4735,9 +4737,9 @@ pub async fn list_customer_payment_method( let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), payment_method_id: pm.payment_method_id.clone(), - customer_id: pm.customer_id, + customer_id: pm.customer_id.clone(), payment_method, - payment_method_type: pm.payment_method_type, + payment_method_type: pm.get_payment_method_subtype(), payment_method_issuer: pm.payment_method_issuer, card: pm_list_context.card_details, metadata: pm.metadata, @@ -5194,6 +5196,10 @@ pub async fn get_lookup_key_from_locker( pub async fn get_masked_bank_details( pm: &domain::PaymentMethod, ) -> errors::RouterResult> { + #[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") + ))] let payment_method_data = pm .payment_method_data .clone() @@ -5208,6 +5214,12 @@ pub async fn get_masked_bank_details( ) .transpose()?; + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + let payment_method_data = pm + .payment_method_data + .clone() + .map(|x| x.into_inner().expose().into_inner()); + match payment_method_data { Some(pmd) => match pmd { PaymentMethodsData::Card(_) => Ok(None), @@ -5223,6 +5235,10 @@ pub async fn get_masked_bank_details( pub async fn get_bank_account_connector_details( pm: &domain::PaymentMethod, ) -> errors::RouterResult> { + #[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") + ))] let payment_method_data = pm .payment_method_data .clone() @@ -5237,6 +5253,12 @@ pub async fn get_bank_account_connector_details( ) .transpose()?; + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + let payment_method_data = pm + .payment_method_data + .clone() + .map(|x| x.into_inner().expose().into_inner()); + match payment_method_data { Some(pmd) => match pmd { PaymentMethodsData::Card(_) => Err(errors::ApiErrorResponse::UnprocessableEntity { @@ -5250,12 +5272,12 @@ pub async fn get_bank_account_connector_details( .ok_or(errors::ApiErrorResponse::InternalServerError)?; let pm_type = pm - .payment_method_type + .get_payment_method_subtype() .get_required_value("payment_method_type") .attach_printable("PaymentMethodType not found")?; let pm = pm - .payment_method + .get_payment_method_type() .get_required_value("payment_method") .attach_printable("PaymentMethod not found")?; @@ -5318,7 +5340,7 @@ pub async fn set_default_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let pm = payment_method - .payment_method + .get_payment_method_type() .get_required_value("payment_method")?; utils::when( @@ -5363,7 +5385,7 @@ pub async fn set_default_payment_method( let resp = CustomerDefaultPaymentMethodResponse { default_payment_method_id: updated_customer_details.default_payment_method_id, customer_id, - payment_method_type: payment_method.payment_method_type, + payment_method_type: payment_method.get_payment_method_subtype(), payment_method: pm, }; @@ -5562,7 +5584,7 @@ pub async fn retrieve_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; - let card = if pm.payment_method == Some(enums::PaymentMethod::Card) { + let card = if pm.get_payment_method_type() == Some(enums::PaymentMethod::Card) { let card_detail = if state.conf.locker.locker_enabled { let card = get_card_from_locker( &state, @@ -5585,11 +5607,11 @@ pub async fn retrieve_payment_method( }; Ok(services::ApplicationResponse::Json( api::PaymentMethodResponse { - merchant_id: pm.merchant_id, - customer_id: Some(pm.customer_id), - payment_method_id: pm.payment_method_id, - payment_method: pm.payment_method, - payment_method_type: pm.payment_method_type, + merchant_id: pm.merchant_id.clone(), + customer_id: Some(pm.customer_id.clone()), + payment_method_id: pm.payment_method_id.clone(), + payment_method: pm.get_payment_method_type(), + payment_method_type: pm.get_payment_method_subtype(), #[cfg(feature = "payouts")] bank_transfer: None, card, @@ -5636,7 +5658,7 @@ pub async fn delete_payment_method( .to_not_found_response(errors::ApiErrorResponse::InternalServerError) .attach_printable("Customer not found for the payment method")?; - if key.payment_method == Some(enums::PaymentMethod::Card) { + if key.get_payment_method_type() == Some(enums::PaymentMethod::Card) { let response = delete_card_from_locker( &state, &key.customer_id, diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index ca1cdf06768..ff52799ed63 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -412,9 +412,10 @@ pub async fn perform_surcharge_decision_management_for_saved_cards( .get_required_value("payment_token") .change_context(ConfigError::InputConstructionError)?; - backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); + backend_input.payment_method.payment_method = + Some(customer_payment_method.payment_method_type); backend_input.payment_method.payment_method_type = - customer_payment_method.payment_method_type; + customer_payment_method.payment_method_subtype; let card_network = match customer_payment_method.payment_method_data.as_ref() { Some(api_models::payment_methods::PaymentMethodListData::Card(card)) => { diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 51a109e8a90..c0f54a30f3f 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -552,11 +552,7 @@ pub fn generate_payment_method_response( let pmd = pm .payment_method_data .clone() - .map(|data| data.into_inner().expose()) - .map(|decrypted_value| decrypted_value.parse_value("PaymentMethodsData")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to parse PaymentMethodsData")? + .map(|data| data.into_inner().expose().into_inner()) .and_then(|data| match data { api::PaymentMethodsData::Card(card) => { Some(api::PaymentMethodResponseData::Card(card.into())) @@ -568,8 +564,8 @@ pub fn generate_payment_method_response( merchant_id: pm.merchant_id.to_owned(), customer_id: pm.customer_id.to_owned(), payment_method_id: pm.id.get_string_repr().to_owned(), - payment_method: pm.payment_method, - payment_method_type: pm.payment_method_type, + payment_method_type: pm.get_payment_method_type(), + payment_method_subtype: pm.get_payment_method_subtype(), created: Some(pm.created_at), recurring_enabled: false, last_used_at: Some(pm.last_used_at), diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index dd481467f16..5ac8f454c21 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -5683,7 +5683,7 @@ pub fn is_network_transaction_id_flow( .connector_list; is_connector_agnostic_mit_enabled == Some(true) - && payment_method_info.payment_method == Some(storage_enums::PaymentMethod::Card) + && payment_method_info.get_payment_method_type() == Some(storage_enums::PaymentMethod::Card) && ntid_supported_connectors.contains(&connector) && payment_method_info.network_transaction_id.is_some() } @@ -5702,7 +5702,7 @@ pub fn is_network_token_with_network_transaction_id_flow( match ( is_connector_agnostic_mit_enabled, is_network_tokenization_enabled, - payment_method_info.payment_method, + payment_method_info.get_payment_method_type(), payment_method_info.network_transaction_id.clone(), payment_method_info.network_token_locker_id.is_some(), payment_method_info diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index f00f03d929f..5dc5ca35469 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -554,8 +554,8 @@ pub async fn get_token_pm_type_mandate_details( ( None, - payment_method_info.payment_method, - payment_method_info.payment_method_type, + payment_method_info.get_payment_method_type(), + payment_method_info.get_payment_method_subtype(), None, None, None, @@ -608,7 +608,7 @@ pub async fn get_token_pm_type_mandate_details( Ok(customer_payment_methods) => Ok(customer_payment_methods .iter() .find(|payment_method| { - payment_method.payment_method_type + payment_method.get_payment_method_subtype() == request.payment_method_type }) .cloned()), @@ -808,13 +808,13 @@ pub async fn get_token_for_recurring_mandate( .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let token = Uuid::new_v4().to_string(); - let payment_method_type = payment_method.payment_method_type; + let payment_method_type = payment_method.get_payment_method_subtype(); let mandate_connector_details = payments::MandateConnectorDetails { connector: mandate.connector, merchant_connector_id: mandate.merchant_connector_id, }; - if let Some(enums::PaymentMethod::Card) = payment_method.payment_method { + if let Some(enums::PaymentMethod::Card) = payment_method.get_payment_method_type() { if state.conf.locker.locker_enabled { let _ = cards::get_lookup_key_from_locker( state, @@ -828,7 +828,7 @@ pub async fn get_token_for_recurring_mandate( if let Some(payment_method_from_request) = req.payment_method { let pm: storage_enums::PaymentMethod = payment_method_from_request; if payment_method - .payment_method + .get_payment_method_type() .is_some_and(|payment_method| payment_method != pm) { Err(report!(errors::ApiErrorResponse::PreconditionFailed { @@ -842,14 +842,14 @@ pub async fn get_token_for_recurring_mandate( Ok(MandateGenericData { token: Some(token), - payment_method: payment_method.payment_method, + payment_method: payment_method.get_payment_method_type(), recurring_mandate_payment_data: Some(RecurringMandatePaymentData { payment_method_type, original_payment_authorized_amount, original_payment_authorized_currency, mandate_metadata: None, }), - payment_method_type: payment_method.payment_method_type, + payment_method_type: payment_method.get_payment_method_subtype(), mandate_connector: Some(mandate_connector_details), mandate_data: None, payment_method_info: Some(payment_method), @@ -857,14 +857,14 @@ pub async fn get_token_for_recurring_mandate( } else { Ok(MandateGenericData { token: None, - payment_method: payment_method.payment_method, + payment_method: payment_method.get_payment_method_type(), recurring_mandate_payment_data: Some(RecurringMandatePaymentData { payment_method_type, original_payment_authorized_amount, original_payment_authorized_currency, mandate_metadata: None, }), - payment_method_type: payment_method.payment_method_type, + payment_method_type: payment_method.get_payment_method_subtype(), mandate_connector: Some(mandate_connector_details), mandate_data: None, payment_method_info: Some(payment_method), @@ -2340,7 +2340,9 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( if payment_data.token_data.is_none() { if let Some(payment_method_info) = &payment_data.payment_method_info { - if payment_method_info.payment_method == Some(storage_enums::PaymentMethod::Card) { + if payment_method_info.get_payment_method_type() + == Some(storage_enums::PaymentMethod::Card) + { payment_data.token_data = Some(storage::PaymentTokenData::PermanentCard(CardTokenData { payment_method_id: Some(payment_method_info.get_id().clone()), diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 9bbe7cca391..8c82c38be71 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -653,7 +653,7 @@ impl GetTracker, api::PaymentsRequest> for Pa .or(payment_attempt.payment_method_type) .or(payment_method_info .as_ref() - .and_then(|pm_info| pm_info.payment_method_type)); + .and_then(|pm_info| pm_info.get_payment_method_subtype())); // The operation merges mandate data from both request and payment_attempt let setup_mandate = mandate_data.map(|mut sm| { diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 6f01af96bb8..4bae04e1426 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -610,7 +610,8 @@ where Ok(customer_payment_methods) => Ok(customer_payment_methods .iter() .find(|payment_method| { - payment_method.payment_method_type == payment_method_type + payment_method.get_payment_method_subtype() + == payment_method_type }) .cloned()), Err(error) => { @@ -1143,6 +1144,7 @@ pub fn update_router_data_with_payment_method_token_result( } } +#[cfg(feature = "v1")] pub fn add_connector_mandate_details_in_payment_method( payment_method_type: Option, authorized_amount: Option, @@ -1174,7 +1176,9 @@ pub fn add_connector_mandate_details_in_payment_method( None } } + #[allow(clippy::too_many_arguments)] +#[cfg(feature = "v1")] pub fn update_connector_mandate_details( mandate_details: Option, payment_method_type: Option, diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index b8672ee79b4..afba73e839c 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -375,7 +375,7 @@ async fn store_bank_details_in_payment_methods( > = HashMap::new(); let key_manager_state = (&state).into(); for pm in payment_methods { - if pm.payment_method == Some(enums::PaymentMethod::BankDebit) + if pm.get_payment_method_type() == Some(enums::PaymentMethod::BankDebit) && pm.payment_method_data.is_some() { let bank_details_pm_data = pm @@ -561,8 +561,8 @@ async fn store_bank_details_in_payment_methods( customer_id: customer_id.clone(), merchant_id: merchant_account.get_id().clone(), id: pm_id, - payment_method: Some(enums::PaymentMethod::BankDebit), - payment_method_type: Some(creds.payment_method_type), + payment_method_type: Some(enums::PaymentMethod::BankDebit), + payment_method_subtype: Some(creds.payment_method_type), status: enums::PaymentMethodStatus::Active, metadata: None, payment_method_data: Some(encrypted_data.into()), diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 764e75df02a..7296a510248 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -148,8 +148,8 @@ pub async fn confirm_payment_method_intent_api( let inner_payload = payment_methods::PaymentMethodIntentConfirmInternal { id: pm_id.clone(), - payment_method: payload.payment_method, payment_method_type: payload.payment_method_type, + payment_method_subtype: payload.payment_method_subtype, client_secret: payload.client_secret.clone(), customer_id: payload.customer_id.to_owned(), payment_method_data: payload.payment_method_data.clone(), diff --git a/crates/router/src/types/api/mandates.rs b/crates/router/src/types/api/mandates.rs index b3bcfa2a887..949eac39745 100644 --- a/crates/router/src/types/api/mandates.rs +++ b/crates/router/src/types/api/mandates.rs @@ -56,7 +56,7 @@ impl MandateResponseExt for MandateResponse { .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let pm = payment_method - .payment_method + .get_payment_method_type() .get_required_value("payment_method") .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) .attach_printable("payment_method not found")?; @@ -91,7 +91,7 @@ impl MandateResponseExt for MandateResponse { None }; let payment_method_type = payment_method - .payment_method_type + .get_payment_method_subtype() .map(|pmt| pmt.to_string()); Ok(Self { mandate_id: mandate.mandate_id, diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index d66bf079d42..8b1175d5cc6 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -6,7 +6,7 @@ pub use api_models::payment_methods::{ PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm, PaymentMethodIntentConfirmInternal, PaymentMethodIntentCreate, - PaymentMethodList, PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse, + PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, @@ -20,7 +20,7 @@ pub use api_models::payment_methods::{ CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, - PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, + PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, @@ -65,8 +65,8 @@ impl PaymentMethodCreateExt for PaymentMethodCreate { fn validate(&self) -> RouterResult<()> { utils::when( !validate_payment_method_type_against_payment_method( - self.payment_method, self.payment_method_type, + self.payment_method_subtype, ), || { Err(report!(errors::ApiErrorResponse::InvalidRequestData { @@ -78,7 +78,7 @@ impl PaymentMethodCreateExt for PaymentMethodCreate { utils::when( !Self::validate_payment_method_data_against_payment_method( - self.payment_method, + self.payment_method_type, self.payment_method_data.clone(), ), || { @@ -97,8 +97,8 @@ impl PaymentMethodCreateExt for PaymentMethodIntentConfirm { fn validate(&self) -> RouterResult<()> { utils::when( !validate_payment_method_type_against_payment_method( - self.payment_method, self.payment_method_type, + self.payment_method_subtype, ), || { Err(report!(errors::ApiErrorResponse::InvalidRequestData { @@ -110,7 +110,7 @@ impl PaymentMethodCreateExt for PaymentMethodIntentConfirm { utils::when( !Self::validate_payment_method_data_against_payment_method( - self.payment_method, + self.payment_method_type, self.payment_method_data.clone(), ), || { diff --git a/crates/router/src/types/domain/types.rs b/crates/router/src/types/domain/types.rs index 082e8df4bfc..d4cd9ef62d7 100644 --- a/crates/router/src/types/domain/types.rs +++ b/crates/router/src/types/domain/types.rs @@ -1,6 +1,6 @@ use common_utils::types::keymanager::KeyManagerState; pub use hyperswitch_domain_models::type_encryption::{ - crypto_operation, AsyncLift, CryptoOperation, Lift, + crypto_operation, AsyncLift, CryptoOperation, Lift, OptionalEncryptableJsonType, }; impl From<&crate::SessionState> for KeyManagerState { diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 36865f5ce09..8ba6783372b 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -101,8 +101,8 @@ impl merchant_id: item.merchant_id.to_owned(), customer_id: Some(item.customer_id.to_owned()), payment_method_id: item.get_id().clone(), - payment_method: item.payment_method, - payment_method_type: item.payment_method_type, + payment_method: item.get_payment_method_type(), + payment_method_type: item.get_payment_method_subtype(), card: card_details, recurring_enabled: false, installment_payment_enabled: false, diff --git a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql index ce485048f4e..30c499e6c42 100644 --- a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql +++ b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/down.sql @@ -10,7 +10,9 @@ ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS direct_debit_token VARCHAR(128), ADD COLUMN IF NOT EXISTS swift_code VARCHAR(32), ADD COLUMN IF NOT EXISTS payment_method_issuer VARCHAR(128), - ADD COLUMN IF NOT EXISTS metadata JSON; + ADD COLUMN IF NOT EXISTS metadata JSON, + ADD COLUMN IF NOT EXISTS payment_method VARCHAR, + ADD COLUMN IF NOT EXISTS payment_method_type VARCHAR(64); CREATE TYPE "PaymentMethodIssuerCode" AS ENUM ( 'jp_hdfc', @@ -27,7 +29,10 @@ CREATE TYPE "PaymentMethodIssuerCode" AS ENUM ( ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS payment_method_issuer_code "PaymentMethodIssuerCode"; -ALTER TABLE payment_methods DROP COLUMN IF EXISTS locker_fingerprint_id; +ALTER TABLE payment_methods + DROP COLUMN IF EXISTS locker_fingerprint_id, + DROP COLUMN IF EXISTS payment_method_type_v2, + DROP COLUMN IF EXISTS payment_method_subtype; ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS payment_method_id VARCHAR(64); UPDATE payment_methods SET payment_method_id = id; diff --git a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql index 23a274b6844..fb319d9f05d 100644 --- a/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql +++ b/v2_migrations/2024-08-23-112510_payment_methods_v2_db_changes/up.sql @@ -12,11 +12,16 @@ ALTER TABLE payment_methods DROP COLUMN IF EXISTS swift_code, DROP COLUMN IF EXISTS payment_method_issuer, DROP COLUMN IF EXISTS payment_method_issuer_code, - DROP COLUMN IF EXISTS metadata; + DROP COLUMN IF EXISTS metadata, + DROP COLUMN IF EXISTS payment_method, + DROP COLUMN IF EXISTS payment_method_type; DROP TYPE IF EXISTS "PaymentMethodIssuerCode"; -ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS locker_fingerprint_id VARCHAR(64); +ALTER TABLE payment_methods + ADD COLUMN IF NOT EXISTS locker_fingerprint_id VARCHAR(64), + ADD COLUMN IF NOT EXISTS payment_method_type_v2 VARCHAR(64), + ADD COLUMN IF NOT EXISTS payment_method_subtype VARCHAR(64); ALTER TABLE payment_methods DROP COLUMN IF EXISTS id; ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS id VARCHAR(64); From e9e8df222c90661493ba974374d70438ce0ffa6f Mon Sep 17 00:00:00 2001 From: Rajnish Date: Tue, 19 Nov 2024 21:52:58 +0530 Subject: [PATCH 10/16] fix(docker-compose): address "role root does not exist" errors arising from postgres health check (#6582) --- docker-compose-development.yml | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose-development.yml b/docker-compose-development.yml index 07a2131c6d4..d7a0e365c36 100644 --- a/docker-compose-development.yml +++ b/docker-compose-development.yml @@ -26,7 +26,7 @@ services: - POSTGRES_PASSWORD=db_pass - POSTGRES_DB=hyperswitch_db healthcheck: - test: ["CMD-SHELL", "pg_isready"] + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] interval: 5s retries: 3 start_period: 5s diff --git a/docker-compose.yml b/docker-compose.yml index f766ff91053..7450a650119 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: - POSTGRES_PASSWORD=db_pass - POSTGRES_DB=hyperswitch_db healthcheck: - test: ["CMD-SHELL", "pg_isready"] + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] interval: 5s retries: 3 start_period: 5s From f3897dd6b57318b681a2c5dc099d787aa8233f24 Mon Sep 17 00:00:00 2001 From: Uzair Khan Date: Tue, 19 Nov 2024 21:54:12 +0530 Subject: [PATCH 11/16] feat(analytics): add `smart_retries` only metrics for analytics v2 dashboard (#6575) --- crates/analytics/src/payments/accumulator.rs | 41 +++++++++++++++++++- crates/api_models/src/analytics/payments.rs | 2 + 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/analytics/src/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs index 5ca9fdf7516..291d7364071 100644 --- a/crates/analytics/src/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -76,8 +76,11 @@ pub struct PaymentsDistributionAccumulator { pub failed: u32, pub total: u32, pub success_without_retries: u32, + pub success_with_only_retries: u32, pub failed_without_retries: u32, + pub failed_with_only_retries: u32, pub total_without_retries: u32, + pub total_with_only_retries: u32, } pub trait PaymentMetricAccumulator { @@ -181,7 +184,14 @@ impl PaymentMetricAccumulator for SuccessRateAccumulator { } impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { - type MetricOutput = (Option, Option, Option, Option); + type MetricOutput = ( + Option, + Option, + Option, + Option, + Option, + Option, + ); fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { if let Some(ref status) = metrics.status { @@ -193,6 +203,8 @@ impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { self.success += success; if metrics.first_attempt.unwrap_or(false) { self.success_without_retries += success; + } else { + self.success_with_only_retries += success; } } } @@ -201,6 +213,8 @@ impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { self.failed += failed; if metrics.first_attempt.unwrap_or(false) { self.failed_without_retries += failed; + } else { + self.failed_with_only_retries += failed; } } } @@ -208,6 +222,8 @@ impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { self.total += total; if metrics.first_attempt.unwrap_or(false) { self.total_without_retries += total; + } else { + self.total_with_only_retries += total; } } } @@ -215,14 +231,17 @@ impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { fn collect(self) -> Self::MetricOutput { if self.total == 0 { - (None, None, None, None) + (None, None, None, None, None, None) } else { let success = Some(self.success); let success_without_retries = Some(self.success_without_retries); + let success_with_only_retries = Some(self.success_with_only_retries); let failed = Some(self.failed); + let failed_with_only_retries = Some(self.failed_with_only_retries); let failed_without_retries = Some(self.failed_without_retries); let total = Some(self.total); let total_without_retries = Some(self.total_without_retries); + let total_with_only_retries = Some(self.total_with_only_retries); let success_rate = match (success, total) { (Some(s), Some(t)) if t > 0 => Some(f64::from(s) * 100.0 / f64::from(t)), @@ -235,6 +254,12 @@ impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { _ => None, }; + let success_rate_with_only_retries = + match (success_with_only_retries, total_with_only_retries) { + (Some(s), Some(t)) if t > 0 => Some(f64::from(s) * 100.0 / f64::from(t)), + _ => None, + }; + let failed_rate = match (failed, total) { (Some(s), Some(t)) if t > 0 => Some(f64::from(s) * 100.0 / f64::from(t)), _ => None, @@ -245,11 +270,19 @@ impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { (Some(s), Some(t)) if t > 0 => Some(f64::from(s) * 100.0 / f64::from(t)), _ => None, }; + + let failed_rate_with_only_retries = + match (failed_with_only_retries, total_with_only_retries) { + (Some(s), Some(t)) if t > 0 => Some(f64::from(s) * 100.0 / f64::from(t)), + _ => None, + }; ( success_rate, success_rate_without_retries, + success_rate_with_only_retries, failed_rate, failed_rate_without_retries, + failed_rate_with_only_retries, ) } } @@ -393,8 +426,10 @@ impl PaymentMetricsAccumulator { let ( payments_success_rate_distribution, payments_success_rate_distribution_without_smart_retries, + payments_success_rate_distribution_with_only_retries, payments_failure_rate_distribution, payments_failure_rate_distribution_without_smart_retries, + payments_failure_rate_distribution_with_only_retries, ) = self.payments_distribution.collect(); let (failure_reason_count, failure_reason_count_without_smart_retries) = self.failure_reasons_distribution.collect(); @@ -413,8 +448,10 @@ impl PaymentMetricsAccumulator { connector_success_rate: self.connector_success_rate.collect(), payments_success_rate_distribution, payments_success_rate_distribution_without_smart_retries, + payments_success_rate_distribution_with_only_retries, payments_failure_rate_distribution, payments_failure_rate_distribution_without_smart_retries, + payments_failure_rate_distribution_with_only_retries, failure_reason_count, failure_reason_count_without_smart_retries, payment_processed_amount_in_usd, diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index b34f8c9293c..1bade6b5ec8 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -283,8 +283,10 @@ pub struct PaymentMetricsBucketValue { pub connector_success_rate: Option, pub payments_success_rate_distribution: Option, pub payments_success_rate_distribution_without_smart_retries: Option, + pub payments_success_rate_distribution_with_only_retries: Option, pub payments_failure_rate_distribution: Option, pub payments_failure_rate_distribution_without_smart_retries: Option, + pub payments_failure_rate_distribution_with_only_retries: Option, pub failure_reason_count: Option, pub failure_reason_count_without_smart_retries: Option, } From 8b31a7bbe1de88f2126bee4547b37cbb16ea95a4 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:26:52 +0530 Subject: [PATCH 12/16] fix(connector): [Adyen]fix error code and message for webhooks response (#6602) Co-authored-by: Chikke Srujan --- .../src/connector/adyen/transformers.rs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index ec97b6ebad5..b6dea0eac21 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -3404,7 +3404,10 @@ pub fn get_adyen_response( > { let status = storage_enums::AttemptStatus::foreign_from((is_capture_manual, response.result_code, pmt)); - let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { + let error = if response.refusal_reason.is_some() + || response.refusal_reason_code.is_some() + || status == storage_enums::AttemptStatus::Failure + { Some(types::ErrorResponse { code: response .refusal_reason_code @@ -3467,7 +3470,10 @@ pub fn get_webhook_response( is_capture_manual, response.status.clone(), ))?; - let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { + let error = if response.refusal_reason.is_some() + || response.refusal_reason_code.is_some() + || status == storage_enums::AttemptStatus::Failure + { Some(types::ErrorResponse { code: response .refusal_reason_code @@ -3533,7 +3539,10 @@ pub fn get_redirection_response( response.result_code.clone(), pmt, )); - let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { + let error = if response.refusal_reason.is_some() + || response.refusal_reason_code.is_some() + || status == storage_enums::AttemptStatus::Failure + { Some(types::ErrorResponse { code: response .refusal_reason_code @@ -3605,7 +3614,10 @@ pub fn get_present_to_shopper_response( response.result_code.clone(), pmt, )); - let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { + let error = if response.refusal_reason.is_some() + || response.refusal_reason_code.is_some() + || status == storage_enums::AttemptStatus::Failure + { Some(types::ErrorResponse { code: response .refusal_reason_code @@ -3663,7 +3675,10 @@ pub fn get_qr_code_response( response.result_code.clone(), pmt, )); - let error = if response.refusal_reason.is_some() || response.refusal_reason_code.is_some() { + let error = if response.refusal_reason.is_some() + || response.refusal_reason_code.is_some() + || status == storage_enums::AttemptStatus::Failure + { Some(types::ErrorResponse { code: response .refusal_reason_code @@ -4431,6 +4446,14 @@ pub struct AdyenIncomingWebhook { impl From for AdyenWebhookResponse { fn from(notif: AdyenNotificationRequestItemWH) -> Self { + let (refusal_reason, refusal_reason_code) = if !is_success_scenario(notif.success.clone()) { + ( + notif.reason.or(Some(consts::NO_ERROR_MESSAGE.to_string())), + Some(consts::NO_ERROR_CODE.to_string()), + ) + } else { + (None, None) + }; Self { transaction_id: notif.psp_reference, payment_reference: notif.original_reference, @@ -4489,8 +4512,8 @@ impl From for AdyenWebhookResponse { currency: notif.amount.currency, }), merchant_reference_id: notif.merchant_reference, - refusal_reason: None, - refusal_reason_code: None, + refusal_reason, + refusal_reason_code, event_code: notif.event_code, } } From 9787a2becf1bc9eceee6a1fec0a4edb5c3e6473b Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:00:49 +0530 Subject: [PATCH 13/16] feat(connector): [Novalnet] Add minimal customer data feature (#6570) --- .../src/connectors/novalnet/transformers.rs | 43 +-- .../payment_connector_required_fields.rs | 245 ------------------ 2 files changed, 25 insertions(+), 263 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index 1888511e9ba..f9bbe8398cb 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -68,11 +68,11 @@ pub struct NovalnetPaymentsRequestMerchant { #[derive(Default, Debug, Serialize, Clone)] pub struct NovalnetPaymentsRequestBilling { - house_no: Secret, - street: Secret, - city: Secret, - zip: Secret, - country_code: api_enums::CountryAlpha2, + house_no: Option>, + street: Option>, + city: Option>, + zip: Option>, + country_code: Option, } #[derive(Default, Debug, Serialize, Clone)] @@ -81,8 +81,9 @@ pub struct NovalnetPaymentsRequestCustomer { last_name: Secret, email: Email, mobile: Option>, - billing: NovalnetPaymentsRequestBilling, - customer_ip: Secret, + billing: Option, + customer_ip: Option>, + no_nc: i64, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] @@ -183,26 +184,32 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym }; let billing = NovalnetPaymentsRequestBilling { - house_no: item.router_data.get_billing_line1()?, - street: item.router_data.get_billing_line2()?, - city: Secret::new(item.router_data.get_billing_city()?), - zip: item.router_data.get_billing_zip()?, - country_code: item.router_data.get_billing_country()?, + house_no: item.router_data.get_optional_billing_line1(), + street: item.router_data.get_optional_billing_line2(), + city: item + .router_data + .get_optional_billing_city() + .map(Secret::new), + zip: item.router_data.get_optional_billing_zip(), + country_code: item.router_data.get_optional_billing_country(), }; let customer_ip = item .router_data .request .get_browser_info()? - .get_ip_address()?; + .get_ip_address() + .ok(); let customer = NovalnetPaymentsRequestCustomer { first_name: item.router_data.get_billing_first_name()?, last_name: item.router_data.get_billing_last_name()?, email: item.router_data.get_billing_email()?, mobile: item.router_data.get_optional_billing_phone_number(), - billing, + billing: Some(billing), customer_ip, + // no_nc is used to indicate if minimal customer data is passed or not + no_nc: 1, }; let lang = item @@ -637,11 +644,11 @@ pub struct NovalnetResponseCustomer { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct NovalnetResponseBilling { - pub city: Secret, - pub country_code: Secret, + pub city: Option>, + pub country_code: Option>, pub house_no: Option>, - pub street: Secret, - pub zip: Secret, + pub street: Option>, + pub zip: Option>, pub state: Option>, } diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index fc37d937d00..8d5a4fe89b9 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -2041,42 +2041,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.line1".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line1".to_string(), - display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressLine1, - value: None, - } - ), - ( - "billing.address.line2".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line2".to_string(), - display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressLine2, - value: None, - } - ), - ( - "billing.address.city".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.city".to_string(), - display_name: "city".to_string(), - field_type: enums::FieldType::UserAddressCity, - value: None, - } - ), - ( - "billing.address.zip".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.zip".to_string(), - display_name: "zip".to_string(), - field_type: enums::FieldType::UserAddressPincode, - value: None, - } - ), ( "billing.address.first_name".to_string(), RequiredFieldInfo { @@ -2104,19 +2068,6 @@ impl Default for settings::RequiredFields { value: None, } ), - ( - "billing.address.country".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.country".to_string(), - display_name: "country".to_string(), - field_type: enums::FieldType::UserAddressCountry{ - options: vec![ - "ALL".to_string(), - ] - }, - value: None, - } - ), ] ), } @@ -5226,42 +5177,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.line1".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line1".to_string(), - display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressLine1, - value: None, - } - ), - ( - "billing.address.line2".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line2".to_string(), - display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressLine2, - value: None, - } - ), - ( - "billing.address.city".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.city".to_string(), - display_name: "city".to_string(), - field_type: enums::FieldType::UserAddressCity, - value: None, - } - ), - ( - "billing.address.zip".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.zip".to_string(), - display_name: "zip".to_string(), - field_type: enums::FieldType::UserAddressPincode, - value: None, - } - ), ( "billing.address.first_name".to_string(), RequiredFieldInfo { @@ -5289,19 +5204,6 @@ impl Default for settings::RequiredFields { value: None, } ), - ( - "billing.address.country".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.country".to_string(), - display_name: "country".to_string(), - field_type: enums::FieldType::UserAddressCountry{ - options: vec![ - "ALL".to_string(), - ] - }, - value: None, - } - ), ] ), } @@ -8203,42 +8105,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.line1".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line1".to_string(), - display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressLine1, - value: None, - } - ), - ( - "billing.address.line2".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line2".to_string(), - display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressLine2, - value: None, - } - ), - ( - "billing.address.city".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.city".to_string(), - display_name: "city".to_string(), - field_type: enums::FieldType::UserAddressCity, - value: None, - } - ), - ( - "billing.address.zip".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.zip".to_string(), - display_name: "zip".to_string(), - field_type: enums::FieldType::UserAddressPincode, - value: None, - } - ), ( "billing.address.first_name".to_string(), RequiredFieldInfo { @@ -8266,19 +8132,6 @@ impl Default for settings::RequiredFields { value: None, } ), - ( - "billing.address.country".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.country".to_string(), - display_name: "country".to_string(), - field_type: enums::FieldType::UserAddressCountry{ - options: vec![ - "ALL".to_string(), - ] - }, - value: None, - } - ), ] ), } @@ -8563,42 +8416,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.line1".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line1".to_string(), - display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressLine1, - value: None, - } - ), - ( - "billing.address.line2".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line2".to_string(), - display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressLine2, - value: None, - } - ), - ( - "billing.address.city".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.city".to_string(), - display_name: "city".to_string(), - field_type: enums::FieldType::UserAddressCity, - value: None, - } - ), - ( - "billing.address.zip".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.zip".to_string(), - display_name: "zip".to_string(), - field_type: enums::FieldType::UserAddressPincode, - value: None, - } - ), ( "billing.address.first_name".to_string(), RequiredFieldInfo { @@ -8626,19 +8443,6 @@ impl Default for settings::RequiredFields { value: None, } ), - ( - "billing.address.country".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.country".to_string(), - display_name: "country".to_string(), - field_type: enums::FieldType::UserAddressCountry{ - options: vec![ - "ALL".to_string(), - ] - }, - value: None, - } - ), ] ), } @@ -9313,42 +9117,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.line1".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line1".to_string(), - display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressLine1, - value: None, - } - ), - ( - "billing.address.line2".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.line2".to_string(), - display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressLine2, - value: None, - } - ), - ( - "billing.address.city".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.city".to_string(), - display_name: "city".to_string(), - field_type: enums::FieldType::UserAddressCity, - value: None, - } - ), - ( - "billing.address.zip".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.zip".to_string(), - display_name: "zip".to_string(), - field_type: enums::FieldType::UserAddressPincode, - value: None, - } - ), ( "billing.address.first_name".to_string(), RequiredFieldInfo { @@ -9376,19 +9144,6 @@ impl Default for settings::RequiredFields { value: None, } ), - ( - "billing.address.country".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.country".to_string(), - display_name: "country".to_string(), - field_type: enums::FieldType::UserAddressCountry{ - options: vec![ - "ALL".to_string(), - ] - }, - value: None, - } - ), ] ), } From 0302c3033fbff4bfbdb18df44fabc3513b063fb0 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:07:36 +0530 Subject: [PATCH 14/16] fix(analytics): fix `authentication_type` and `card_last_4` fields serialization for payment_intent_filters (#6595) --- crates/api_models/src/analytics/payment_intents.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs index dd51c97d935..365abd71edc 100644 --- a/crates/api_models/src/analytics/payment_intents.rs +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -63,11 +63,15 @@ pub enum PaymentIntentDimensions { Currency, ProfileId, Connector, + #[strum(serialize = "authentication_type")] + #[serde(rename = "authentication_type")] AuthType, PaymentMethod, PaymentMethodType, CardNetwork, MerchantId, + #[strum(serialize = "card_last_4")] + #[serde(rename = "card_last_4")] CardLast4, CardIssuer, ErrorReason, From 607b3df3fc822a5f937dbb4f89fbdb0352eca3ff Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:49:09 +0530 Subject: [PATCH 15/16] feat(routing): add invalidate window as a service for SR based routing (#6264) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/development.toml | 4 + .../src/grpc_client/dynamic_routing.rs | 37 ++++-- crates/router/src/core/routing.rs | 36 +++++- crates/router/src/routes/app.rs | 77 ++++++------ crates/router/src/routes/routing.rs | 4 +- proto/success_rate.proto | 115 ++++++++++-------- 6 files changed, 170 insertions(+), 103 deletions(-) diff --git a/config/development.toml b/config/development.toml index b6638fe1d47..6a6fda6755e 100644 --- a/config/development.toml +++ b/config/development.toml @@ -782,3 +782,7 @@ card_networks = "Visa, AmericanExpress, Mastercard" [network_tokenization_supported_connectors] connector_list = "cybersource" + +[grpc_client.dynamic_routing_client] +host = "localhost" +port = 7000 diff --git a/crates/external_services/src/grpc_client/dynamic_routing.rs b/crates/external_services/src/grpc_client/dynamic_routing.rs index 0546d05ba7c..3264f065b51 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing.rs @@ -14,8 +14,9 @@ use serde; use success_rate::{ success_rate_calculator_client::SuccessRateCalculatorClient, CalSuccessRateConfig, CalSuccessRateRequest, CalSuccessRateResponse, - CurrentBlockThreshold as DynamicCurrentThreshold, LabelWithStatus, - UpdateSuccessRateWindowConfig, UpdateSuccessRateWindowRequest, UpdateSuccessRateWindowResponse, + CurrentBlockThreshold as DynamicCurrentThreshold, InvalidateWindowsRequest, + InvalidateWindowsResponse, LabelWithStatus, UpdateSuccessRateWindowConfig, + UpdateSuccessRateWindowRequest, UpdateSuccessRateWindowResponse, }; use tonic::Status; #[allow( @@ -111,6 +112,11 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { params: String, response: Vec, ) -> DynamicRoutingResult; + /// To invalidates the success rate routing keys + async fn invalidate_success_rate_routing_keys( + &self, + id: String, + ) -> DynamicRoutingResult; } #[async_trait::async_trait] @@ -139,9 +145,8 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { config, }); - let mut client = self.clone(); - - let response = client + let response = self + .clone() .fetch_success_rate(request) .await .change_context(DynamicRoutingError::SuccessRateBasedRoutingFailure( @@ -179,9 +184,8 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { config, }); - let mut client = self.clone(); - - let response = client + let response = self + .clone() .update_success_rate_window(request) .await .change_context(DynamicRoutingError::SuccessRateBasedRoutingFailure( @@ -191,6 +195,23 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { Ok(response) } + + async fn invalidate_success_rate_routing_keys( + &self, + id: String, + ) -> DynamicRoutingResult { + let request = tonic::Request::new(InvalidateWindowsRequest { id }); + + let response = self + .clone() + .invalidate_windows(request) + .await + .change_context(DynamicRoutingError::SuccessRateBasedRoutingFailure( + "Failed to invalidate the success rate routing keys".to_string(), + ))? + .into_inner(); + Ok(response) + } } impl ForeignTryFrom for DynamicCurrentThreshold { diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 94eb9bd741e..348f5229b75 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -7,14 +7,17 @@ use api_models::{ routing::{self as routing_types, RoutingRetrieveQuery}, }; use async_trait::async_trait; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use common_utils::ext_traits::AsyncExt; use diesel_models::routing_algorithm::RoutingAlgorithm; use error_stack::ResultExt; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use external_services::grpc_client::dynamic_routing::SuccessBasedDynamicRouting; use hyperswitch_domain_models::{mandates, payment_address}; -#[cfg(feature = "v1")] -use router_env::logger; -use router_env::metrics::add_attributes; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use router_env::{logger, metrics::add_attributes}; use rustc_hash::FxHashSet; -#[cfg(feature = "v1")] +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] use storage_impl::redis::cache; #[cfg(feature = "payouts")] @@ -1182,7 +1185,7 @@ pub async fn update_default_routing_config_for_profile( )) } -#[cfg(feature = "v1")] +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] pub async fn toggle_success_based_routing( state: SessionState, merchant_account: domain::MerchantAccount, @@ -1379,7 +1382,7 @@ pub async fn toggle_success_based_routing( } } -#[cfg(feature = "v1")] +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] pub async fn success_based_routing_update_configs( state: SessionState, request: routing_types::SuccessBasedRoutingConfig, @@ -1449,6 +1452,27 @@ pub async fn success_based_routing_update_configs( 1, &add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]), ); + + let prefix_of_dynamic_routing_keys = helpers::generate_tenant_business_profile_id( + &state.tenant.redis_key_prefix, + profile_id.get_string_repr(), + ); + state + .grpc_client + .dynamic_routing + .success_rate_client + .as_ref() + .async_map(|sr_client| async { + sr_client + .invalidate_success_rate_routing_keys(prefix_of_dynamic_routing_keys) + .await + .change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Failed to invalidate the routing keys".to_string(), + }) + }) + .await + .transpose()?; + Ok(service_api::ApplicationResponse::Json(new_record)) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5c81db35ea9..1d7e9727cf3 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1724,47 +1724,54 @@ impl Profile { #[cfg(all(feature = "olap", feature = "v1"))] impl Profile { pub fn server(state: AppState) -> Scope { - web::scope("/account/{account_id}/business_profile") + let mut route = web::scope("/account/{account_id}/business_profile") .app_data(web::Data::new(state)) .service( web::resource("") .route(web::post().to(profiles::profile_create)) .route(web::get().to(profiles::profiles_list)), - ) - .service( - web::scope("/{profile_id}") - .service( - web::scope("/dynamic_routing").service( - web::scope("/success_based") - .service( - web::resource("/toggle").route( - web::post().to(routing::toggle_success_based_routing), - ), - ) - .service(web::resource("/config/{algorithm_id}").route( - web::patch().to(|state, req, path, payload| { - routing::success_based_routing_update_configs( - state, req, path, payload, - ) - }), - )), - ), - ) - .service( - web::resource("") - .route(web::get().to(profiles::profile_retrieve)) - .route(web::post().to(profiles::profile_update)) - .route(web::delete().to(profiles::profile_delete)), - ) - .service( - web::resource("/toggle_extended_card_info") - .route(web::post().to(profiles::toggle_extended_card_info)), - ) - .service( - web::resource("/toggle_connector_agnostic_mit") - .route(web::post().to(profiles::toggle_connector_agnostic_mit)), + ); + + #[cfg(feature = "dynamic_routing")] + { + route = + route.service( + web::scope("/{profile_id}/dynamic_routing").service( + web::scope("/success_based") + .service( + web::resource("/toggle") + .route(web::post().to(routing::toggle_success_based_routing)), + ) + .service(web::resource("/config/{algorithm_id}").route( + web::patch().to(|state, req, path, payload| { + routing::success_based_routing_update_configs( + state, req, path, payload, + ) + }), + )), ), - ) + ); + } + + route = route.service( + web::scope("/{profile_id}") + .service( + web::resource("") + .route(web::get().to(profiles::profile_retrieve)) + .route(web::post().to(profiles::profile_update)) + .route(web::delete().to(profiles::profile_delete)), + ) + .service( + web::resource("/toggle_extended_card_info") + .route(web::post().to(profiles::toggle_extended_card_info)), + ) + .service( + web::resource("/toggle_connector_agnostic_mit") + .route(web::post().to(profiles::toggle_connector_agnostic_mit)), + ), + ); + + route } } diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index b861b54b5b5..cb7744f5200 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -1009,7 +1009,7 @@ pub async fn routing_update_default_config_for_profile( .await } -#[cfg(all(feature = "olap", feature = "v1"))] +#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))] #[instrument(skip_all)] pub async fn toggle_success_based_routing( state: web::Data, @@ -1052,7 +1052,7 @@ pub async fn toggle_success_based_routing( .await } -#[cfg(all(feature = "olap", feature = "v1"))] +#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))] #[instrument(skip_all)] pub async fn success_based_routing_update_configs( state: web::Data, diff --git a/proto/success_rate.proto b/proto/success_rate.proto index 8018f6d5fe4..38e56e36c0f 100644 --- a/proto/success_rate.proto +++ b/proto/success_rate.proto @@ -1,57 +1,68 @@ syntax = "proto3"; package success_rate; - service SuccessRateCalculator { - rpc FetchSuccessRate (CalSuccessRateRequest) returns (CalSuccessRateResponse); - - rpc UpdateSuccessRateWindow (UpdateSuccessRateWindowRequest) returns (UpdateSuccessRateWindowResponse); - } - - // API-1 types - message CalSuccessRateRequest { - string id = 1; - string params = 2; - repeated string labels = 3; - CalSuccessRateConfig config = 4; - } - - message CalSuccessRateConfig { - uint32 min_aggregates_size = 1; - double default_success_rate = 2; - } - - message CalSuccessRateResponse { - repeated LabelWithScore labels_with_score = 1; - } - - message LabelWithScore { - double score = 1; - string label = 2; - } +service SuccessRateCalculator { + rpc FetchSuccessRate (CalSuccessRateRequest) returns (CalSuccessRateResponse); + + rpc UpdateSuccessRateWindow (UpdateSuccessRateWindowRequest) returns (UpdateSuccessRateWindowResponse); + + rpc InvalidateWindows (InvalidateWindowsRequest) returns (InvalidateWindowsResponse); +} + +// API-1 types +message CalSuccessRateRequest { + string id = 1; + string params = 2; + repeated string labels = 3; + CalSuccessRateConfig config = 4; +} + +message CalSuccessRateConfig { + uint32 min_aggregates_size = 1; + double default_success_rate = 2; +} + +message CalSuccessRateResponse { + repeated LabelWithScore labels_with_score = 1; +} + +message LabelWithScore { + double score = 1; + string label = 2; +} // API-2 types - message UpdateSuccessRateWindowRequest { - string id = 1; - string params = 2; - repeated LabelWithStatus labels_with_status = 3; - UpdateSuccessRateWindowConfig config = 4; - } - - message LabelWithStatus { - string label = 1; - bool status = 2; - } - - message UpdateSuccessRateWindowConfig { - uint32 max_aggregates_size = 1; - CurrentBlockThreshold current_block_threshold = 2; - } - - message CurrentBlockThreshold { - optional uint64 duration_in_mins = 1; - uint64 max_total_count = 2; - } - - message UpdateSuccessRateWindowResponse { - string message = 1; - } \ No newline at end of file +message UpdateSuccessRateWindowRequest { + string id = 1; + string params = 2; + repeated LabelWithStatus labels_with_status = 3; + UpdateSuccessRateWindowConfig config = 4; +} + +message LabelWithStatus { + string label = 1; + bool status = 2; +} + +message UpdateSuccessRateWindowConfig { + uint32 max_aggregates_size = 1; + CurrentBlockThreshold current_block_threshold = 2; +} + +message CurrentBlockThreshold { + optional uint64 duration_in_mins = 1; + uint64 max_total_count = 2; +} + +message UpdateSuccessRateWindowResponse { + string message = 1; +} + + // API-3 types +message InvalidateWindowsRequest { + string id = 1; +} + +message InvalidateWindowsResponse { + string message = 1; +} \ No newline at end of file From 43d87913ab3d177a6d193b3e475c96609cc09a28 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 00:21:16 +0000 Subject: [PATCH 16/16] chore(version): 2024.11.20.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 640a259efa9..2ad0c05a08f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.20.0 + +### Features + +- **analytics:** Add `smart_retries` only metrics for analytics v2 dashboard ([#6575](https://github.com/juspay/hyperswitch/pull/6575)) ([`f3897dd`](https://github.com/juspay/hyperswitch/commit/f3897dd6b57318b681a2c5dc099d787aa8233f24)) +- **connector:** [Novalnet] Add minimal customer data feature ([#6570](https://github.com/juspay/hyperswitch/pull/6570)) ([`9787a2b`](https://github.com/juspay/hyperswitch/commit/9787a2becf1bc9eceee6a1fec0a4edb5c3e6473b)) +- **router:** Add payment incoming webhooks support for v2 ([#6551](https://github.com/juspay/hyperswitch/pull/6551)) ([`8e9c3ec`](https://github.com/juspay/hyperswitch/commit/8e9c3ec8931851dae638037b91eb1611399be0bf)) +- **routing:** Add invalidate window as a service for SR based routing ([#6264](https://github.com/juspay/hyperswitch/pull/6264)) ([`607b3df`](https://github.com/juspay/hyperswitch/commit/607b3df3fc822a5f937dbb4f89fbdb0352eca3ff)) + +### Bug Fixes + +- **analytics:** Fix `authentication_type` and `card_last_4` fields serialization for payment_intent_filters ([#6595](https://github.com/juspay/hyperswitch/pull/6595)) ([`0302c30`](https://github.com/juspay/hyperswitch/commit/0302c3033fbff4bfbdb18df44fabc3513b063fb0)) +- **connector:** + - [Worldpay] use 4 digit expiry year ([#6543](https://github.com/juspay/hyperswitch/pull/6543)) ([`e730a2e`](https://github.com/juspay/hyperswitch/commit/e730a2ee5a35d56f3740e923cb16de67edca2fc0)) + - [Adyen]fix error code and message for webhooks response ([#6602](https://github.com/juspay/hyperswitch/pull/6602)) ([`8b31a7b`](https://github.com/juspay/hyperswitch/commit/8b31a7bbe1de88f2126bee4547b37cbb16ea95a4)) +- **docker-compose:** Address "role root does not exist" errors arising from postgres health check ([#6582](https://github.com/juspay/hyperswitch/pull/6582)) ([`e9e8df2`](https://github.com/juspay/hyperswitch/commit/e9e8df222c90661493ba974374d70438ce0ffa6f)) + +### Refactors + +- **payment_methods_v2:** Rename `payment_method` and `payment_method_type` fields and use concrete type for `payment_method_data` ([#6555](https://github.com/juspay/hyperswitch/pull/6555)) ([`11e9241`](https://github.com/juspay/hyperswitch/commit/11e92413b22f13df8cfa62020d48d490e37b5d87)) +- **users:** Force 2FA in production environment ([#6596](https://github.com/juspay/hyperswitch/pull/6596)) ([`bbd55e3`](https://github.com/juspay/hyperswitch/commit/bbd55e32f838349b402e8cd0abc06d34f647be94)) + +**Full Changelog:** [`2024.11.19.0...2024.11.20.0`](https://github.com/juspay/hyperswitch/compare/2024.11.19.0...2024.11.20.0) + +- - - + ## 2024.11.19.0 ### Features