diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 1e901161b978..7f59ad24c0f6 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -4166,6 +4166,10 @@ "is_tax_connector_enabled": { "type": "boolean", "description": "Indicates if tax_calculator connector is enabled or not.\nIf set to `true` tax_connector_id will be checked." + }, + "is_network_tokenization_enabled": { + "type": "boolean", + "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked." } }, "additionalProperties": false @@ -4178,7 +4182,8 @@ "profile_name", "enable_payment_response_hash", "redirect_to_merchant_with_http_post", - "is_tax_connector_enabled" + "is_tax_connector_enabled", + "is_network_tokenization_enabled" ], "properties": { "merchant_id": { @@ -4348,6 +4353,12 @@ "is_tax_connector_enabled": { "type": "boolean", "description": "Indicates if tax_calculator connector is enabled or not.\nIf set to `true` tax_connector_id will be checked." + }, + "is_network_tokenization_enabled": { + "type": "boolean", + "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked.", + "default": false, + "example": false } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index cc212de0c3bc..58c8bb9afb42 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -7959,6 +7959,10 @@ "is_tax_connector_enabled": { "type": "boolean", "description": "Indicates if tax_calculator connector is enabled or not.\nIf set to `true` tax_connector_id will be checked." + }, + "is_network_tokenization_enabled": { + "type": "boolean", + "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked." } }, "additionalProperties": false @@ -7971,7 +7975,8 @@ "profile_name", "enable_payment_response_hash", "redirect_to_merchant_with_http_post", - "is_tax_connector_enabled" + "is_tax_connector_enabled", + "is_network_tokenization_enabled" ], "properties": { "merchant_id": { @@ -8150,6 +8155,12 @@ "is_tax_connector_enabled": { "type": "boolean", "description": "Indicates if tax_calculator connector is enabled or not.\nIf set to `true` tax_connector_id will be checked." + }, + "is_network_tokenization_enabled": { + "type": "boolean", + "description": "Indicates if is_network_tokenization_enabled is enabled or not.\nIf set to `true` is_network_tokenization_enabled will be checked.", + "default": false, + "example": false } } }, diff --git a/config/config.example.toml b/config/config.example.toml index 7059626f8cfd..be0532622bb9 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -730,3 +730,19 @@ connector_list = "" [recipient_emails] recon = "test@example.com" + +[network_tokenization_supported_card_networks] +card_networks = "Visa, AmericanExpress, Mastercard" # Supported card networks for network tokenization + +[network_tokenization_service] # Network Tokenization Service Configuration +generate_token_url= "" # base url to generate token +fetch_token_url= "" # base url to fetch token +token_service_api_key= "" # api key for token service +public_key= "" # public key to encrypt data for token service +private_key= "" # private key to decrypt response payload from token service +key_id= "" # key id to encrypt data for token service +delete_token_url= "" # base url to delete token from token service +check_token_status_url= "" # base url to check token status from token service + +[network_tokenization_supported_connectors] +connector_list = "cybersource" # Supported connectors for network tokenization \ No newline at end of file diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 6fe08509b761..9bbf137d2ced 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -302,3 +302,13 @@ encryption_key = "user_auth_table_encryption_key" # Encryption key used for encr [recipient_emails] recon = "recon@example.com" + +[network_tokenization_service] # Network Tokenization Service Configuration +generate_token_url= "" # base url to generate token +fetch_token_url= "" # base url to fetch token +token_service_api_key= "" # api key for token service +public_key= "" # public key to encrypt data for token service +private_key= "" # private key to decrypt response payload from token service +key_id= "" # key id to encrypt data for token service +delete_token_url= "" # base url to delete token from token service +check_token_status_url= "" # base url to check token status from token service \ No newline at end of file diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 90d29a6992dc..abb50a2c2f06 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -373,4 +373,10 @@ keys = "accept-language,user-agent" sdk_eligible_payment_methods = "card" [locker_based_open_banking_connectors] -connector_list = "" \ No newline at end of file +connector_list = "" + +[network_tokenization_supported_card_networks] +card_networks = "Visa, AmericanExpress, Mastercard" + +[network_tokenization_supported_connectors] +connector_list = "cybersource" \ No newline at end of file diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 0443d1915427..38799c238586 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -386,4 +386,10 @@ keys = "accept-language,user-agent" sdk_eligible_payment_methods = "card" [locker_based_open_banking_connectors] -connector_list = "" \ No newline at end of file +connector_list = "" + +[network_tokenization_supported_card_networks] +card_networks = "Visa, AmericanExpress, Mastercard" + +[network_tokenization_supported_connectors] +connector_list = "cybersource" \ No newline at end of file diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 8c6792417296..2217a72429c2 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -390,4 +390,10 @@ keys = "accept-language,user-agent" sdk_eligible_payment_methods = "card" [locker_based_open_banking_connectors] -connector_list = "" \ No newline at end of file +connector_list = "" + +[network_tokenization_supported_card_networks] +card_networks = "Visa, AmericanExpress, Mastercard" + +[network_tokenization_supported_connectors] +connector_list = "cybersource" diff --git a/config/development.toml b/config/development.toml index 1c6a4a827f31..f9f1ebc1202e 100644 --- a/config/development.toml +++ b/config/development.toml @@ -734,3 +734,9 @@ connector_list = "" [recipient_emails] recon = "recon@example.com" + +[network_tokenization_supported_card_networks] +card_networks = "Visa, AmericanExpress, Mastercard" + +[network_tokenization_supported_connectors] +connector_list = "cybersource" \ No newline at end of file diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 704acdfee147..698800f082dc 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -593,3 +593,19 @@ connector_list = "" [recipient_emails] recon = "recon@example.com" + +[network_tokenization_supported_card_networks] +card_networks = "Visa, AmericanExpress, Mastercard" + +[network_tokenization_service] +generate_token_url= "" +fetch_token_url= "" +token_service_api_key= "" +public_key= "" +private_key= "" +key_id= "" +delete_token_url= "" +check_token_status_url= "" + +[network_tokenization_supported_connectors] +connector_list = "cybersource" diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index c6f541dba027..ef00c22433c3 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1917,6 +1917,11 @@ pub struct BusinessProfileCreate { /// If set to `true` tax_connector_id will be checked. #[serde(default)] pub is_tax_connector_enabled: bool, + + /// Indicates if is_network_tokenization_enabled is enabled or not. + /// If set to `true` is_network_tokenization_enabled will be checked. + #[serde(default)] + pub is_network_tokenization_enabled: bool, } #[nutype::nutype( @@ -2021,6 +2026,11 @@ pub struct BusinessProfileCreate { /// If set to `true` tax_connector_id will be checked. #[serde(default)] pub is_tax_connector_enabled: bool, + + /// Indicates if is_network_tokenization_enabled is enabled or not. + /// If set to `true` is_network_tokenization_enabled will be checked. + #[serde(default)] + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -2138,6 +2148,11 @@ pub struct BusinessProfileResponse { /// Indicates if tax_calculator connector is enabled or not. /// If set to `true` tax_connector_id will be checked. pub is_tax_connector_enabled: bool, + + /// Indicates if is_network_tokenization_enabled is enabled or not. + /// If set to `true` is_network_tokenization_enabled will be checked. + #[schema(default = false, example = false)] + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v2")] @@ -2246,6 +2261,11 @@ pub struct BusinessProfileResponse { /// Indicates if tax_calculator connector is enabled or not. /// If set to `true` tax_connector_id will be checked. pub is_tax_connector_enabled: bool, + + /// Indicates if is_network_tokenization_enabled is enabled or not. + /// If set to `true` is_network_tokenization_enabled will be checked. + #[schema(default = false, example = false)] + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -2359,6 +2379,9 @@ pub struct BusinessProfileUpdate { /// Indicates if dynamic routing is enabled or not. #[serde(default)] pub dynamic_routing_algorithm: Option, + + /// Indicates if is_network_tokenization_enabled is enabled or not. + pub is_network_tokenization_enabled: Option, } #[cfg(feature = "v2")] @@ -2459,6 +2482,9 @@ pub struct BusinessProfileUpdate { /// Indicates if tax_calculator connector is enabled or not. /// If set to `true` tax_connector_id will be checked. pub is_tax_connector_enabled: Option, + + /// Indicates if is_network_tokenization_enabled is enabled or not. + pub is_network_tokenization_enabled: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index abd795361bb8..01a786f523b9 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1026,7 +1026,15 @@ pub struct MandateIds { #[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] pub enum MandateReferenceId { ConnectorMandateId(ConnectorMandateReferenceId), // mandate_id send by connector - NetworkMandateId(String), // network_txns_id send by Issuer to connector, Used for PG agnostic mandate txns + NetworkMandateId(String), // network_txns_id send by Issuer to connector, Used for PG agnostic mandate txns along with card data + NetworkTokenWithNTI(NetworkTokenWithNTIRef), // network_txns_id send by Issuer to connector, Used for PG agnostic mandate txns along with network token data +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] +pub struct NetworkTokenWithNTIRef { + pub network_transaction_id: String, + pub token_exp_month: Option>, + pub token_exp_year: Option>, } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 10c9dae2cc10..653275bd5600 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1781,16 +1781,27 @@ pub enum MandateStatus { )] #[router_derive::diesel_enum(storage_type = "text")] pub enum CardNetwork { + #[serde(alias = "VISA")] Visa, + #[serde(alias = "MASTERCARD")] Mastercard, + #[serde(alias = "AMERICANEXPRESS")] + #[serde(alias = "AMEX")] AmericanExpress, JCB, + #[serde(alias = "DINERSCLUB")] DinersClub, + #[serde(alias = "DISCOVER")] Discover, + #[serde(alias = "CARTESBANCAIRES")] CartesBancaires, + #[serde(alias = "UNIONPAY")] UnionPay, + #[serde(alias = "INTERAC")] Interac, + #[serde(alias = "RUPAY")] RuPay, + #[serde(alias = "MAESTRO")] Maestro, } diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index a72263b96a8a..0051d0a8631f 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -54,6 +54,7 @@ pub struct BusinessProfile { pub is_tax_connector_enabled: Option, pub version: common_enums::ApiVersion, pub dynamic_routing_algorithm: Option, + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -94,6 +95,7 @@ pub struct BusinessProfileNew { pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, pub version: common_enums::ApiVersion, + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -131,6 +133,7 @@ pub struct BusinessProfileUpdateInternal { pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, pub dynamic_routing_algorithm: Option, + pub is_network_tokenization_enabled: Option, } #[cfg(feature = "v1")] @@ -167,6 +170,7 @@ impl BusinessProfileUpdateInternal { tax_connector_id, is_tax_connector_enabled, dynamic_routing_algorithm, + is_network_tokenization_enabled, } = self; BusinessProfile { profile_id: source.profile_id, @@ -222,6 +226,8 @@ impl BusinessProfileUpdateInternal { version: source.version, dynamic_routing_algorithm: dynamic_routing_algorithm .or(source.dynamic_routing_algorithm), + is_network_tokenization_enabled: is_network_tokenization_enabled + .unwrap_or(source.is_network_tokenization_enabled), } } } @@ -272,6 +278,7 @@ pub struct BusinessProfile { pub id: common_utils::id_type::ProfileId, pub version: common_enums::ApiVersion, pub dynamic_routing_algorithm: Option, + pub is_network_tokenization_enabled: bool, } impl BusinessProfile { @@ -326,6 +333,7 @@ pub struct BusinessProfileNew { pub default_fallback_routing: Option, pub id: common_utils::id_type::ProfileId, pub version: common_enums::ApiVersion, + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v2")] @@ -364,6 +372,7 @@ pub struct BusinessProfileUpdateInternal { pub frm_routing_algorithm_id: Option, pub payout_routing_algorithm_id: Option, pub default_fallback_routing: Option, + pub is_network_tokenization_enabled: Option, } #[cfg(feature = "v2")] @@ -401,6 +410,7 @@ impl BusinessProfileUpdateInternal { frm_routing_algorithm_id, payout_routing_algorithm_id, default_fallback_routing, + is_network_tokenization_enabled, } = self; BusinessProfile { id: source.id, @@ -459,6 +469,8 @@ impl BusinessProfileUpdateInternal { default_fallback_routing: default_fallback_routing.or(source.default_fallback_routing), version: source.version, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: is_network_tokenization_enabled + .unwrap_or(source.is_network_tokenization_enabled), } } } @@ -510,6 +522,7 @@ impl From for BusinessProfile { default_fallback_routing: new.default_fallback_routing, version: new.version, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: new.is_network_tokenization_enabled, } } } diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index c56ab8a0b2bb..ca919eb15daf 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -59,6 +59,9 @@ pub struct PaymentMethod { pub payment_method_billing_address: Option, pub updated_by: Option, pub version: common_enums::ApiVersion, + pub network_token_requestor_reference_id: Option, + pub network_token_locker_id: Option, + pub network_token_payment_method_data: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -87,6 +90,9 @@ pub struct PaymentMethod { pub locker_fingerprint_id: Option, pub id: String, pub version: common_enums::ApiVersion, + pub network_token_requestor_reference_id: Option, + pub network_token_locker_id: Option, + pub network_token_payment_method_data: Option, } impl PaymentMethod { @@ -144,6 +150,9 @@ pub struct PaymentMethodNew { pub payment_method_billing_address: Option, pub updated_by: Option, pub version: common_enums::ApiVersion, + pub network_token_requestor_reference_id: Option, + pub network_token_locker_id: Option, + pub network_token_payment_method_data: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -172,6 +181,9 @@ pub struct PaymentMethodNew { pub locker_fingerprint_id: Option, pub id: String, pub version: common_enums::ApiVersion, + pub network_token_requestor_reference_id: Option, + pub network_token_locker_id: Option, + pub network_token_payment_method_data: Option, } impl PaymentMethodNew { @@ -233,6 +245,9 @@ pub enum PaymentMethodUpdate { payment_method: Option, payment_method_type: Option, payment_method_issuer: Option, + network_token_requestor_reference_id: Option, + network_token_locker_id: Option, + network_token_payment_method_data: Option, }, ConnectorMandateDetailsUpdate { connector_mandate_details: Option, @@ -269,6 +284,9 @@ pub enum PaymentMethodUpdate { locker_id: Option, payment_method: Option, payment_method_type: Option, + network_token_requestor_reference_id: Option, + network_token_locker_id: Option, + network_token_payment_method_data: Option, }, ConnectorMandateDetailsUpdate { connector_mandate_details: Option, @@ -301,6 +319,9 @@ pub struct PaymentMethodUpdateInternal { updated_by: Option, payment_method_type: Option, last_modified: PrimitiveDateTime, + network_token_requestor_reference_id: Option, + network_token_locker_id: Option, + network_token_payment_method_data: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -352,12 +373,15 @@ pub struct PaymentMethodUpdateInternal { network_transaction_id: Option, status: Option, locker_id: Option, + network_token_requestor_reference_id: Option, payment_method: Option, connector_mandate_details: Option, updated_by: Option, payment_method_type: Option, payment_method_issuer: Option, last_modified: PrimitiveDateTime, + network_token_locker_id: Option, + network_token_payment_method_data: Option, } #[cfg(all( @@ -416,12 +440,15 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status: None, locker_id: None, + network_token_requestor_reference_id: None, payment_method: None, connector_mandate_details: None, updated_by: None, payment_method_issuer: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data, @@ -432,12 +459,15 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status: None, locker_id: None, + network_token_requestor_reference_id: None, payment_method: None, connector_mandate_details: None, updated_by: None, payment_method_issuer: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { metadata: None, @@ -446,12 +476,15 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status: None, locker_id: None, + network_token_requestor_reference_id: None, payment_method: None, connector_mandate_details: None, updated_by: None, payment_method_issuer: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::UpdatePaymentMethodDataAndLastUsed { payment_method_data, @@ -463,12 +496,15 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status: None, locker_id: None, + network_token_requestor_reference_id: None, payment_method: None, connector_mandate_details: None, updated_by: None, payment_method_issuer: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate { network_transaction_id, @@ -480,12 +516,15 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id, status, locker_id: None, + network_token_requestor_reference_id: None, payment_method: None, connector_mandate_details: None, updated_by: None, payment_method_issuer: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::StatusUpdate { status } => Self { metadata: None, @@ -494,20 +533,26 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status, locker_id: None, + network_token_requestor_reference_id: None, payment_method: None, connector_mandate_details: None, updated_by: None, payment_method_issuer: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::AdditionalDataUpdate { payment_method_data, status, locker_id, + network_token_requestor_reference_id, payment_method, payment_method_type, payment_method_issuer, + network_token_locker_id, + network_token_payment_method_data, } => Self { metadata: None, payment_method_data, @@ -515,12 +560,15 @@ impl From for PaymentMethodUpdateInternal { network_transaction_id: None, status, locker_id, + network_token_requestor_reference_id, payment_method, connector_mandate_details: None, updated_by: None, payment_method_issuer, payment_method_type, last_modified: common_utils::date_time::now(), + network_token_locker_id, + network_token_payment_method_data, }, PaymentMethodUpdate::ConnectorMandateDetailsUpdate { connector_mandate_details, @@ -530,6 +578,7 @@ impl From for PaymentMethodUpdateInternal { last_used_at: None, status: None, locker_id: None, + network_token_requestor_reference_id: None, payment_method: None, connector_mandate_details, network_transaction_id: None, @@ -537,6 +586,8 @@ impl From for PaymentMethodUpdateInternal { payment_method_issuer: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_payment_method_data: None, }, } } @@ -561,6 +612,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_requestor_reference_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data, @@ -576,6 +630,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_requestor_reference_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { metadata: None, @@ -589,6 +646,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_requestor_reference_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::UpdatePaymentMethodDataAndLastUsed { payment_method_data, @@ -605,6 +665,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_requestor_reference_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate { network_transaction_id, @@ -621,6 +684,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_requestor_reference_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::StatusUpdate { status } => Self { metadata: None, @@ -634,6 +700,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_requestor_reference_id: None, + network_token_payment_method_data: None, }, PaymentMethodUpdate::AdditionalDataUpdate { payment_method_data, @@ -641,6 +710,9 @@ impl From for PaymentMethodUpdateInternal { locker_id, payment_method, payment_method_type, + network_token_requestor_reference_id, + network_token_locker_id, + network_token_payment_method_data, } => Self { metadata: None, payment_method_data, @@ -653,6 +725,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type, last_modified: common_utils::date_time::now(), + network_token_requestor_reference_id, + network_token_locker_id, + network_token_payment_method_data, }, PaymentMethodUpdate::ConnectorMandateDetailsUpdate { connector_mandate_details, @@ -668,6 +743,9 @@ impl From for PaymentMethodUpdateInternal { updated_by: None, payment_method_type: None, last_modified: common_utils::date_time::now(), + network_token_locker_id: None, + network_token_requestor_reference_id: None, + network_token_payment_method_data: None, }, } } @@ -684,6 +762,9 @@ impl From<&PaymentMethodNew> for PaymentMethod { merchant_id: payment_method_new.merchant_id.clone(), payment_method_id: payment_method_new.payment_method_id.clone(), locker_id: payment_method_new.locker_id.clone(), + network_token_requestor_reference_id: payment_method_new + .network_token_requestor_reference_id + .clone(), accepted_currency: payment_method_new.accepted_currency.clone(), scheme: payment_method_new.scheme.clone(), token: payment_method_new.token.clone(), @@ -713,6 +794,10 @@ impl From<&PaymentMethodNew> for PaymentMethod { .payment_method_billing_address .clone(), version: payment_method_new.version, + network_token_locker_id: payment_method_new.network_token_locker_id.clone(), + network_token_payment_method_data: payment_method_new + .network_token_payment_method_data + .clone(), } } } @@ -743,6 +828,13 @@ impl From<&PaymentMethodNew> for PaymentMethod { id: payment_method_new.id.clone(), locker_fingerprint_id: payment_method_new.locker_fingerprint_id.clone(), version: payment_method_new.version, + network_token_requestor_reference_id: payment_method_new + .network_token_requestor_reference_id + .clone(), + network_token_locker_id: payment_method_new.network_token_locker_id.clone(), + network_token_payment_method_data: payment_method_new + .network_token_payment_method_data + .clone(), } } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index affae09f01ec..239a2d5f2b91 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -210,6 +210,7 @@ diesel::table! { is_tax_connector_enabled -> Nullable, version -> ApiVersion, dynamic_routing_algorithm -> Nullable, + is_network_tokenization_enabled -> Bool, } } @@ -1009,6 +1010,11 @@ diesel::table! { #[max_length = 64] updated_by -> Nullable, version -> ApiVersion, + #[max_length = 128] + network_token_requestor_reference_id -> Nullable, + #[max_length = 64] + network_token_locker_id -> Nullable, + network_token_payment_method_data -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index e71e406c205a..ae3c38823932 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -217,6 +217,7 @@ diesel::table! { id -> Varchar, version -> ApiVersion, dynamic_routing_algorithm -> Nullable, + is_network_tokenization_enabled -> Bool, } } @@ -965,6 +966,11 @@ diesel::table! { #[max_length = 64] id -> Varchar, version -> ApiVersion, + #[max_length = 128] + network_token_requestor_reference_id -> Nullable, + #[max_length = 64] + network_token_locker_id -> Nullable, + network_token_payment_method_data -> Nullable, } } diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index bda133e65b70..a60a0d73ff3c 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1077,7 +1077,9 @@ impl PaymentsAuthorizeRequestData for PaymentsAuthorizeData { Some(payments::MandateReferenceId::ConnectorMandateId(connector_mandate_ids)) => { connector_mandate_ids.connector_mandate_id.clone() } - Some(payments::MandateReferenceId::NetworkMandateId(_)) | None => None, + Some(payments::MandateReferenceId::NetworkMandateId(_)) + | None + | Some(payments::MandateReferenceId::NetworkTokenWithNTI(_)) => None, }) } fn is_mandate_payment(&self) -> bool { diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index b13d79175c21..159c894f17b6 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -55,6 +55,7 @@ pub struct BusinessProfile { pub is_tax_connector_enabled: bool, pub version: common_enums::ApiVersion, pub dynamic_routing_algorithm: Option, + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -92,6 +93,7 @@ pub struct BusinessProfileSetter { pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, pub dynamic_routing_algorithm: Option, + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v1")] @@ -136,6 +138,7 @@ impl From for BusinessProfile { is_tax_connector_enabled: value.is_tax_connector_enabled, version: consts::API_VERSION, dynamic_routing_algorithm: value.dynamic_routing_algorithm, + is_network_tokenization_enabled: value.is_network_tokenization_enabled, } } } @@ -182,6 +185,7 @@ pub struct BusinessProfileGeneralUpdate { pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, pub dynamic_routing_algorithm: Option, + pub is_network_tokenization_enabled: Option, } #[cfg(feature = "v1")] @@ -201,6 +205,9 @@ pub enum BusinessProfileUpdate { ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled: Option, }, + NetworkTokenizationUpdate { + is_network_tokenization_enabled: Option, + }, } #[cfg(feature = "v1")] @@ -238,6 +245,7 @@ impl From for BusinessProfileUpdateInternal { tax_connector_id, is_tax_connector_enabled, dynamic_routing_algorithm, + is_network_tokenization_enabled, } = *update; Self { @@ -272,6 +280,7 @@ impl From for BusinessProfileUpdateInternal { tax_connector_id, is_tax_connector_enabled, dynamic_routing_algorithm, + is_network_tokenization_enabled, } } BusinessProfileUpdate::RoutingAlgorithmUpdate { @@ -308,6 +317,7 @@ impl From for BusinessProfileUpdateInternal { tax_connector_id: None, is_tax_connector_enabled: None, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: None, }, BusinessProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -342,6 +352,7 @@ impl From for BusinessProfileUpdateInternal { tax_connector_id: None, is_tax_connector_enabled: None, dynamic_routing_algorithm, + is_network_tokenization_enabled: None, }, BusinessProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -376,6 +387,7 @@ impl From for BusinessProfileUpdateInternal { tax_connector_id: None, is_tax_connector_enabled: None, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: None, }, BusinessProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -410,6 +422,42 @@ impl From for BusinessProfileUpdateInternal { tax_connector_id: None, is_tax_connector_enabled: None, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: None, + }, + BusinessProfileUpdate::NetworkTokenizationUpdate { + is_network_tokenization_enabled, + } => Self { + profile_name: None, + modified_at: now, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + routing_algorithm: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + is_recon_enabled: None, + applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, + authentication_connector_details: None, + payout_link_config: None, + is_extended_card_info_enabled: None, + extended_card_info_config: None, + is_connector_agnostic_mit_enabled: None, + use_billing_as_payment_method_billing: None, + collect_shipping_details_from_wallet_connector: None, + collect_billing_details_from_wallet_connector: None, + outgoing_webhook_custom_http_headers: None, + always_collect_billing_details_from_wallet_connector: None, + always_collect_shipping_details_from_wallet_connector: None, + tax_connector_id: None, + is_tax_connector_enabled: None, + dynamic_routing_algorithm: None, + is_network_tokenization_enabled, }, } } @@ -463,6 +511,7 @@ impl super::behaviour::Conversion for BusinessProfile { is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, dynamic_routing_algorithm: self.dynamic_routing_algorithm, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }) } @@ -528,6 +577,7 @@ impl super::behaviour::Conversion for BusinessProfile { is_tax_connector_enabled: item.is_tax_connector_enabled.unwrap_or(false), version: item.version, dynamic_routing_algorithm: item.dynamic_routing_algorithm, + is_network_tokenization_enabled: item.is_network_tokenization_enabled, }) } .await @@ -577,6 +627,7 @@ impl super::behaviour::Conversion for BusinessProfile { tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }) } } @@ -619,6 +670,7 @@ pub struct BusinessProfile { pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, pub version: common_enums::ApiVersion, + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v2")] @@ -657,6 +709,7 @@ pub struct BusinessProfileSetter { pub default_fallback_routing: Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, + pub is_network_tokenization_enabled: bool, } #[cfg(feature = "v2")] @@ -702,6 +755,7 @@ impl From for BusinessProfile { tax_connector_id: value.tax_connector_id, is_tax_connector_enabled: value.is_tax_connector_enabled, version: consts::API_VERSION, + is_network_tokenization_enabled: value.is_network_tokenization_enabled, } } } @@ -751,6 +805,7 @@ pub struct BusinessProfileGeneralUpdate { pub always_collect_shipping_details_from_wallet_connector: Option, pub order_fulfillment_time: Option, pub order_fulfillment_time_origin: Option, + pub is_network_tokenization_enabled: Option, } #[cfg(feature = "v2")] @@ -770,6 +825,9 @@ pub enum BusinessProfileUpdate { ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled: Option, }, + NetworkTokenizationUpdate { + is_network_tokenization_enabled: Option, + }, } #[cfg(feature = "v2")] @@ -802,6 +860,7 @@ impl From for BusinessProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector, order_fulfillment_time, order_fulfillment_time_origin, + is_network_tokenization_enabled, } = *update; Self { profile_name, @@ -836,6 +895,7 @@ impl From for BusinessProfileUpdateInternal { default_fallback_routing: None, tax_connector_id: None, is_tax_connector_enabled: None, + is_network_tokenization_enabled, } } BusinessProfileUpdate::RoutingAlgorithmUpdate { @@ -873,6 +933,7 @@ impl From for BusinessProfileUpdateInternal { default_fallback_routing: None, tax_connector_id: None, is_tax_connector_enabled: None, + is_network_tokenization_enabled: None, }, BusinessProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -908,6 +969,7 @@ impl From for BusinessProfileUpdateInternal { default_fallback_routing: None, tax_connector_id: None, is_tax_connector_enabled: None, + is_network_tokenization_enabled: None, }, BusinessProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -943,6 +1005,7 @@ impl From for BusinessProfileUpdateInternal { default_fallback_routing: None, tax_connector_id: None, is_tax_connector_enabled: None, + is_network_tokenization_enabled: None, }, BusinessProfileUpdate::DefaultRoutingFallbackUpdate { default_fallback_routing, @@ -978,6 +1041,43 @@ impl From for BusinessProfileUpdateInternal { default_fallback_routing, tax_connector_id: None, is_tax_connector_enabled: None, + is_network_tokenization_enabled: None, + }, + BusinessProfileUpdate::NetworkTokenizationUpdate { + is_network_tokenization_enabled, + } => Self { + profile_name: None, + modified_at: now, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + is_recon_enabled: None, + applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, + authentication_connector_details: None, + payout_link_config: None, + is_extended_card_info_enabled: None, + extended_card_info_config: None, + is_connector_agnostic_mit_enabled: None, + use_billing_as_payment_method_billing: None, + collect_shipping_details_from_wallet_connector: None, + collect_billing_details_from_wallet_connector: None, + outgoing_webhook_custom_http_headers: None, + always_collect_billing_details_from_wallet_connector: None, + always_collect_shipping_details_from_wallet_connector: None, + routing_algorithm_id: None, + payout_routing_algorithm_id: None, + order_fulfillment_time: None, + order_fulfillment_time_origin: None, + frm_routing_algorithm_id: None, + default_fallback_routing: None, + tax_connector_id: None, + is_tax_connector_enabled: None, + is_network_tokenization_enabled, }, } } @@ -1033,6 +1133,7 @@ impl super::behaviour::Conversion for BusinessProfile { is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }) } @@ -1099,6 +1200,7 @@ impl super::behaviour::Conversion for BusinessProfile { tax_connector_id: item.tax_connector_id, is_tax_connector_enabled: item.is_tax_connector_enabled.unwrap_or(false), version: item.version, + is_network_tokenization_enabled: item.is_network_tokenization_enabled, }) } .await @@ -1150,6 +1252,7 @@ impl super::behaviour::Conversion for BusinessProfile { tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), version: self.version, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }) } } diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index b64c745eec0f..b21bedd880c2 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -499,7 +499,7 @@ pub struct NetworkTokenData { pub token_number: cards::CardNumber, pub token_exp_month: Secret, pub token_exp_year: Secret, - pub token_cryptogram: Secret, + pub token_cryptogram: Option>, pub card_issuer: Option, pub card_network: Option, pub card_type: Option, diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index ef8d90d0e43e..e7cbae9e88c8 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -51,6 +51,9 @@ pub struct PaymentMethod { pub payment_method_billing_address: OptionalEncryptableValue, pub updated_by: Option, pub version: common_enums::ApiVersion, + pub network_token_requestor_reference_id: Option, + pub network_token_locker_id: Option, + pub network_token_payment_method_data: OptionalEncryptableValue, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -76,6 +79,9 @@ pub struct PaymentMethod { pub locker_fingerprint_id: Option, pub id: String, pub version: common_enums::ApiVersion, + pub network_token_requestor_reference_id: Option, + pub network_token_locker_id: Option, + pub network_token_payment_method_data: OptionalEncryptableValue, } impl PaymentMethod { @@ -136,6 +142,11 @@ impl super::behaviour::Conversion for PaymentMethod { .map(|val| val.into()), updated_by: self.updated_by, version: self.version, + network_token_requestor_reference_id: self.network_token_requestor_reference_id, + network_token_locker_id: self.network_token_locker_id, + network_token_payment_method_data: self + .network_token_payment_method_data + .map(|val| val.into()), }) } @@ -207,6 +218,22 @@ impl super::behaviour::Conversion for PaymentMethod { .await?, updated_by: item.updated_by, version: item.version, + network_token_requestor_reference_id: item.network_token_requestor_reference_id, + network_token_locker_id: item.network_token_locker_id, + network_token_payment_method_data: item + .network_token_payment_method_data + .async_lift(|inner| async { + crypto_operation( + state, + type_name!(Self::DstType), + CryptoOperation::DecryptOptional(inner), + key_manager_identifier.clone(), + key.peek(), + ) + .await + .and_then(|val| val.try_into_optionaloperation()) + }) + .await?, }) } .await @@ -250,6 +277,11 @@ impl super::behaviour::Conversion for PaymentMethod { .map(|val| val.into()), updated_by: self.updated_by, version: self.version, + network_token_requestor_reference_id: self.network_token_requestor_reference_id, + network_token_locker_id: self.network_token_locker_id, + network_token_payment_method_data: self + .network_token_payment_method_data + .map(|val| val.into()), }) } } @@ -283,6 +315,11 @@ impl super::behaviour::Conversion for PaymentMethod { updated_by: self.updated_by, locker_fingerprint_id: self.locker_fingerprint_id, version: self.version, + network_token_requestor_reference_id: self.network_token_requestor_reference_id, + network_token_locker_id: self.network_token_locker_id, + network_token_payment_method_data: self + .network_token_payment_method_data + .map(|val| val.into()), }) } @@ -343,6 +380,22 @@ impl super::behaviour::Conversion for PaymentMethod { updated_by: item.updated_by, locker_fingerprint_id: item.locker_fingerprint_id, version: item.version, + network_token_requestor_reference_id: item.network_token_requestor_reference_id, + network_token_locker_id: item.network_token_locker_id, + network_token_payment_method_data: item + .network_token_payment_method_data + .async_lift(|inner| async { + crypto_operation( + state, + type_name!(Self::DstType), + CryptoOperation::DecryptOptional(inner), + key_manager_identifier.clone(), + key.peek(), + ) + .await + .and_then(|val| val.try_into_optionaloperation()) + }) + .await?, }) } .await @@ -375,6 +428,11 @@ impl super::behaviour::Conversion for PaymentMethod { updated_by: self.updated_by, locker_fingerprint_id: self.locker_fingerprint_id, version: self.version, + network_token_requestor_reference_id: self.network_token_requestor_reference_id, + network_token_locker_id: self.network_token_locker_id, + network_token_payment_method_data: self + .network_token_payment_method_data + .map(|val| val.into()), }) } } diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index ce9c030333b7..9b0bfbb40b21 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -1,4 +1,4 @@ -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use hyperswitch_interfaces::secrets_interface::{ secret_handler::SecretsHandler, secret_state::{RawSecret, SecretStateContainer, SecuredSecret}, @@ -288,6 +288,32 @@ impl SecretsHandler for settings::UserAuthMethodSettings { } } +#[async_trait::async_trait] +impl SecretsHandler for settings::NetworkTokenizationService { + async fn convert_to_raw_secret( + value: SecretStateContainer, + secret_management_client: &dyn SecretManagementInterface, + ) -> CustomResult, SecretsManagementError> { + let network_tokenization = value.get_inner(); + let token_service_api_key = secret_management_client + .get_secret(network_tokenization.token_service_api_key.clone()) + .await?; + let public_key = secret_management_client + .get_secret(network_tokenization.public_key.clone()) + .await?; + let private_key = secret_management_client + .get_secret(network_tokenization.private_key.clone()) + .await?; + + Ok(value.transition_state(|network_tokenization| Self { + public_key, + private_key, + token_service_api_key, + ..network_tokenization + })) + } +} + /// # Panics /// /// Will panic even if kms decryption fails for at least one field @@ -386,6 +412,19 @@ pub(crate) async fn fetch_raw_secrets( .await .expect("Failed to decrypt user_auth_methods configs"); + #[allow(clippy::expect_used)] + let network_tokenization_service = conf + .network_tokenization_service + .async_map(|network_tokenization_service| async { + settings::NetworkTokenizationService::convert_to_raw_secret( + network_tokenization_service, + secret_management_client, + ) + .await + .expect("Failed to decrypt network tokenization service configs") + }) + .await; + Settings { server: conf.server, master_database, @@ -459,5 +498,9 @@ pub(crate) async fn fetch_raw_secrets( decision: conf.decision, locker_based_open_banking_connectors: conf.locker_based_open_banking_connectors, recipient_emails: conf.recipient_emails, + network_tokenization_supported_card_networks: conf + .network_tokenization_supported_card_networks, + network_tokenization_service, + network_tokenization_supported_connectors: conf.network_tokenization_supported_connectors, } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 0a65d935f500..78ff709cc02d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -121,6 +121,9 @@ pub struct Settings { pub decision: Option, pub locker_based_open_banking_connectors: LockerBasedRecipientConnectorList, pub recipient_emails: RecipientMails, + pub network_tokenization_supported_card_networks: NetworkTokenizationSupportedCardNetworks, + pub network_tokenization_service: Option>, + pub network_tokenization_supported_connectors: NetworkTokenizationSupportedConnectors, } #[derive(Debug, Deserialize, Clone, Default)] @@ -394,6 +397,24 @@ pub struct NetworkTransactionIdSupportedConnectors { pub connector_list: HashSet, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct NetworkTokenizationSupportedCardNetworks { + #[serde(deserialize_with = "deserialize_hashset")] + pub card_networks: HashSet, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct NetworkTokenizationService { + pub generate_token_url: url::Url, + pub fetch_token_url: url::Url, + pub token_service_api_key: Secret, + pub public_key: Secret, + pub private_key: Secret, + pub key_id: String, + pub delete_token_url: url::Url, + pub check_token_status_url: url::Url, +} + #[derive(Debug, Deserialize, Clone)] pub struct SupportedPaymentMethodsForMandate( pub HashMap, @@ -716,6 +737,12 @@ pub struct UserAuthMethodSettings { pub encryption_key: Secret, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct NetworkTokenizationSupportedConnectors { + #[serde(deserialize_with = "deserialize_hashset")] + pub connector_list: HashSet, +} + impl Settings { pub fn new() -> ApplicationResult { Self::with_config_path(None) @@ -825,6 +852,12 @@ impl Settings { .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?; self.generic_link.payment_method_collect.validate()?; self.generic_link.payout_link.validate()?; + + self.network_tokenization_service + .as_ref() + .map(|x| x.get_inner().validate()) + .transpose()?; + Ok(()) } } diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 441172b05cca..67db7b1266cd 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -192,3 +192,33 @@ impl super::settings::GenericLinkEnvConfig { }) } } + +impl super::settings::NetworkTokenizationService { + pub fn validate(&self) -> Result<(), ApplicationError> { + use common_utils::fp_utils::when; + + when(self.token_service_api_key.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "token_service_api_key must not be empty".into(), + )) + })?; + + when(self.public_key.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "public_key must not be empty".into(), + )) + })?; + + when(self.key_id.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "key_id must not be empty".into(), + )) + })?; + + when(self.private_key.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "private_key must not be empty".into(), + )) + }) + } +} diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 2539c5c52f97..e915d05c358a 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2567,6 +2567,12 @@ impl<'a> } } } + payments::MandateReferenceId::NetworkTokenWithNTI(_) => { + Err(errors::ConnectorError::NotSupported { + message: "Network tokenization for payment method".to_string(), + connector: "Adyen", + })? + } }?; Ok(AdyenPaymentRequest { amount, diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index e04fe3e3fba9..57563c276f5b 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -502,6 +502,11 @@ impl TryFrom<&AuthorizedotnetRouterData<&types::PaymentsAuthorizeRouterData>> Some(api_models::payments::MandateReferenceId::ConnectorMandateId( connector_mandate_id, )) => TransactionRequest::try_from((item, connector_mandate_id))?, + Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_)) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("authorizedotnet"), + ))? + } None => { match &item.router_data.request.payment_method_data { domain::PaymentMethodData::Card(ccard) => { diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 6af3c6006577..fc35cab773a6 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -16,9 +16,10 @@ use serde_json::Value; use crate::connector::utils::PayoutsData; use crate::{ connector::utils::{ - self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData, - PaymentsCompleteAuthorizeRequestData, PaymentsPreProcessingData, - PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RecurringMandateData, RouterData, + self, AddressDetailsData, ApplePayDecrypt, CardData, NetworkTokenData, + PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData, + PaymentsPreProcessingData, PaymentsSetupMandateRequestData, PaymentsSyncRequestData, + RecurringMandateData, RouterData, }, consts, core::errors, @@ -368,6 +369,22 @@ pub struct CaptureOptions { total_capture_count: u32, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkTokenizedCard { + number: cards::CardNumber, + expiration_month: Secret, + expiration_year: Secret, + cryptogram: Option>, + transaction_type: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkTokenPaymentInformation { + tokenized_card: NetworkTokenizedCard, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CardPaymentInformation { @@ -433,6 +450,7 @@ pub enum PaymentInformation { ApplePay(Box), ApplePayToken(Box), MandatePayment(Box), + NetworkToken(Box), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -726,6 +744,76 @@ impl }), ) } + Some(payments::MandateReferenceId::NetworkTokenWithNTI(mandate_data)) => { + let (original_amount, original_currency) = match network + .clone() + .map(|network| network.to_lowercase()) + .as_deref() + { + //This is to make original_authorized_amount mandatory for discover card networks in NetworkMandateId flow + Some("004") => { + let original_amount = Some( + item.router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_amount()?, + ); + let original_currency = Some( + item.router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_currency()?, + ); + (original_amount, original_currency) + } + _ => { + let original_amount = item + .router_data + .recurring_mandate_payment_data + .as_ref() + .and_then(|recurring_mandate_payment_data| { + recurring_mandate_payment_data + .original_payment_authorized_amount + }); + + let original_currency = item + .router_data + .recurring_mandate_payment_data + .as_ref() + .and_then(|recurring_mandate_payment_data| { + recurring_mandate_payment_data + .original_payment_authorized_currency + }); + + (original_amount, original_currency) + } + }; + let original_authorized_amount = match original_amount.zip(original_currency) { + Some((original_amount, original_currency)) => Some( + utils::to_currency_base_unit(original_amount, original_currency)?, + ), + None => None, + }; + commerce_indicator = "recurring".to_string(); // + ( + None, + None, + Some(CybersourceAuthorizationOptions { + initiator: Some(CybersourcePaymentInitiator { + initiator_type: Some(CybersourcePaymentInitiatorTypes::Merchant), + credential_stored_on_file: None, + stored_credential_used: Some(true), + }), + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: Some("7".to_string()), // 7 is for MIT using NTI + original_authorized_amount, + previous_transaction_id: Some(Secret::new( + mandate_data.network_transaction_id, + )), + }), + ignore_avs_result: connector_merchant_config.disable_avs, + ignore_cv_result: connector_merchant_config.disable_cvn, + }), + ) + } None => (None, None, None), } } else { @@ -1114,6 +1202,87 @@ impl } } +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + domain::NetworkTokenData, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, token_data): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + domain::NetworkTokenData, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_optional_billing(), email)?; + let order_information = OrderInformationWithBill::from((item, Some(bill_to))); + + let card_issuer = token_data.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + + let payment_information = + PaymentInformation::NetworkToken(Box::new(NetworkTokenPaymentInformation { + tokenized_card: NetworkTokenizedCard { + number: token_data.token_number, + expiration_month: token_data.token_exp_month, + expiration_year: token_data.token_exp_year, + cryptogram: token_data.token_cryptogram.clone(), + transaction_type: "1".to_string(), + }, + })); + + let processing_information = ProcessingInformation::try_from((item, None, card_type))?; + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = item + .router_data + .request + .metadata + .clone() + .map(Vec::::foreign_from); + + let consumer_authentication_information = item + .router_data + .request + .authentication_data + .as_ref() + .map(|authn_data| { + let (ucaf_authentication_data, cavv) = + if token_data.card_network == Some(common_enums::CardNetwork::Mastercard) { + (Some(Secret::new(authn_data.cavv.clone())), None) + } else { + (None, Some(authn_data.cavv.clone())) + }; + CybersourceConsumerAuthInformation { + ucaf_collection_indicator: None, + cavv, + ucaf_authentication_data, + xid: Some(authn_data.threeds_server_transaction_id.clone()), + directory_server_transaction_id: authn_data + .ds_trans_id + .clone() + .map(Secret::new), + specification_version: None, + pa_specification_version: Some(authn_data.message_version.clone()), + veres_enrolled: Some("Y".to_string()), + } + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information, + merchant_defined_information, + }) + } +} + impl TryFrom<( &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, @@ -1463,6 +1632,9 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> )?; Self::try_from((item, connector_mandate_id)) } + domain::PaymentMethodData::NetworkToken(token_data) => { + Self::try_from((item, token_data)) + } domain::PaymentMethodData::CardRedirect(_) | domain::PaymentMethodData::PayLater(_) | domain::PaymentMethodData::BankRedirect(_) @@ -1475,8 +1647,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> | domain::PaymentMethodData::Voucher(_) | domain::PaymentMethodData::GiftCard(_) | domain::PaymentMethodData::OpenBanking(_) - | domain::PaymentMethodData::CardToken(_) - | domain::PaymentMethodData::NetworkToken(_) => { + | domain::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), ) diff --git a/crates/router/src/connector/razorpay/transformers.rs b/crates/router/src/connector/razorpay/transformers.rs index 69d6bdce3848..621c0969aff0 100644 --- a/crates/router/src/connector/razorpay/transformers.rs +++ b/crates/router/src/connector/razorpay/transformers.rs @@ -152,7 +152,7 @@ pub struct MerchantAccount { reverse_token_enabled: Option, webhook_configs: Option, last_modified: Option, - token_locker_id: Option, + network_token_locker_id: Option, enable_sending_last_four_digits: Option, website: Option, mobile: Option, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 9670e00ee9d0..4da93e2cf017 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -703,7 +703,9 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { Some(payments::MandateReferenceId::ConnectorMandateId(connector_mandate_ids)) => { connector_mandate_ids.connector_mandate_id.clone() } - Some(payments::MandateReferenceId::NetworkMandateId(_)) | None => None, + Some(payments::MandateReferenceId::NetworkMandateId(_)) + | None + | Some(payments::MandateReferenceId::NetworkTokenWithNTI(_)) => None, }) } } @@ -845,7 +847,9 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { Some(payments::MandateReferenceId::ConnectorMandateId(connector_mandate_ids)) => { connector_mandate_ids.connector_mandate_id.clone() } - Some(payments::MandateReferenceId::NetworkMandateId(_)) | None => None, + Some(payments::MandateReferenceId::NetworkMandateId(_)) + | None + | Some(payments::MandateReferenceId::NetworkTokenWithNTI(_)) => None, }) } @@ -856,7 +860,9 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { Some(payments::MandateReferenceId::NetworkMandateId(network_transaction_id)) => { Some(network_transaction_id.clone()) } - Some(payments::MandateReferenceId::ConnectorMandateId(_)) | None => None, + Some(payments::MandateReferenceId::ConnectorMandateId(_)) + | Some(payments::MandateReferenceId::NetworkTokenWithNTI(_)) + | None => None, }) } @@ -2995,3 +3001,21 @@ pub fn get_refund_integrity_object( refund_amount: refund_amount_in_minor_unit, }) } +pub trait NetworkTokenData { + fn get_card_issuer(&self) -> Result; + fn get_expiry_year_4_digit(&self) -> Secret; +} + +impl NetworkTokenData for domain::NetworkTokenData { + fn get_card_issuer(&self) -> Result { + get_card_issuer(self.token_number.peek()) + } + + fn get_expiry_year_4_digit(&self) -> Secret { + let mut year = self.token_exp_year.peek().clone(); + if year.len() == 2 { + year = format!("20{}", year); + } + Secret::new(year) + } +} diff --git a/crates/router/src/connector/wellsfargo/transformers.rs b/crates/router/src/connector/wellsfargo/transformers.rs index d368504a5fc6..761fc43982b4 100644 --- a/crates/router/src/connector/wellsfargo/transformers.rs +++ b/crates/router/src/connector/wellsfargo/transformers.rs @@ -690,7 +690,9 @@ impl }), ) } - None => (None, None, None), + Some(payments::MandateReferenceId::NetworkTokenWithNTI(_)) | None => { + (None, None, None) + } } } else { (None, None, None) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 2579b4ea1615..8ed2eb48b1bd 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3479,6 +3479,7 @@ impl BusinessProfileCreateBridge for api::BusinessProfileCreate { always_collect_shipping_details_from_wallet_connector: self .always_collect_shipping_details_from_wallet_connector, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }, )) } @@ -3583,6 +3584,7 @@ impl BusinessProfileCreateBridge for api::BusinessProfileCreate { default_fallback_routing: None, tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: self.is_tax_connector_enabled, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }, )) } @@ -3827,6 +3829,7 @@ impl BusinessProfileUpdateBridge for api::BusinessProfileUpdate { tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: self.is_tax_connector_enabled, dynamic_routing_algorithm: self.dynamic_routing_algorithm, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }, ))) } @@ -3919,6 +3922,7 @@ impl BusinessProfileUpdateBridge for api::BusinessProfileUpdate { .always_collect_billing_details_from_wallet_connector, always_collect_shipping_details_from_wallet_connector: self .always_collect_shipping_details_from_wallet_connector, + is_network_tokenization_enabled: self.is_network_tokenization_enabled, }, ))) } diff --git a/crates/router/src/core/blocklist/transformers.rs b/crates/router/src/core/blocklist/transformers.rs index d17322f2be5d..b5b3d6625e14 100644 --- a/crates/router/src/core/blocklist/transformers.rs +++ b/crates/router/src/core/blocklist/transformers.rs @@ -15,7 +15,7 @@ use crate::{ payment_methods::transformers as payment_methods, }, headers, routes, - services::{api as services, encryption}, + services::{api as services, encryption, EncryptionAlgorithm}, types::{storage, transformers::ForeignFrom}, utils::ConnectorResponseExt, }; @@ -87,10 +87,11 @@ async fn generate_jwe_payload_for_request( } }; - let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error on jwe encrypt")?; + let jwe_encrypted = + encryption::encrypt_jwe(&payload, public_key, EncryptionAlgorithm::A256GCM, None) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error on jwe encrypt")?; let jwe_payload: Vec<&str> = jwe_encrypted.split('.').collect(); let generate_jwe_body = |payload: Vec<&str>| -> Option { diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index ea4becd93588..e4eb60982d4b 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -20,7 +20,7 @@ use crate::utils::CustomerAddress; use crate::{ core::{ errors::{self, StorageErrorExt}, - payment_methods::cards, + payment_methods::{cards, network_tokenization}, }, db::StorageInterface, pii::PeekInterface, @@ -789,7 +789,22 @@ impl CustomerDeleteBridge for customers::CustomerId { ) .await .switch()?; + + if let Some(network_token_ref_id) = pm.network_token_requestor_reference_id + { + network_tokenization::delete_network_token_from_locker_and_token_service( + state, + &self.customer_id, + merchant_account.get_id(), + pm.payment_method_id.clone(), + pm.network_token_locker_id, + network_token_ref_id, + ) + .await + .switch()?; + } } + db.delete_payment_method_by_merchant_id_payment_method_id( key_manager_state, key_store, diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 7fc3e9a9559a..d8a28ebdf2e2 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -333,3 +333,19 @@ pub enum ConditionalConfigError { #[error("Error constructing the Input")] InputConstructionError, } + +#[derive(Debug, thiserror::Error)] +pub enum NetworkTokenizationError { + #[error("Failed to save network token in vault")] + SaveNetworkTokenFailed, + #[error("Failed to fetch network token details from vault")] + FetchNetworkTokenFailed, + #[error("Failed to encode network token vault request")] + RequestEncodingFailed, + #[error("Failed to deserialize network token service response")] + ResponseDeserializationFailed, + #[error("Failed to delete network token")] + DeleteNetworkTokenFailed, + #[error("Network token service not configured")] + NetworkTokenizationServiceNotConfigured, +} diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 9a39a3c341b7..72fbc3d3d4e9 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -1,5 +1,6 @@ pub mod cards; pub mod migration; +pub mod network_tokenization; pub mod surcharge_decision_configs; pub mod transformers; pub mod utils; @@ -475,12 +476,16 @@ pub async fn retrieve_payment_method_with_token( _card_token_data: Option<&domain::CardToken>, _customer: &Option, _storage_scheme: common_enums::enums::MerchantStorageScheme, + _mandate_id: Option, + _payment_method_info: Option, + _business_profile: &domain::BusinessProfile, ) -> RouterResult { todo!() } #[cfg(feature = "v1")] #[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] pub async fn retrieve_payment_method_with_token( state: &SessionState, merchant_key_store: &domain::MerchantKeyStore, @@ -489,6 +494,9 @@ pub async fn retrieve_payment_method_with_token( card_token_data: Option<&domain::CardToken>, customer: &Option, storage_scheme: common_enums::enums::MerchantStorageScheme, + mandate_id: Option, + payment_method_info: Option, + business_profile: &domain::BusinessProfile, ) -> RouterResult { let token = match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { @@ -541,6 +549,9 @@ pub async fn retrieve_payment_method_with_token( card_token_data, merchant_key_store, storage_scheme, + mandate_id, + payment_method_info, + business_profile, ) .await .map(|card| Some((card, enums::PaymentMethod::Card)))? @@ -572,6 +583,9 @@ pub async fn retrieve_payment_method_with_token( card_token_data, merchant_key_store, storage_scheme, + mandate_id, + payment_method_info, + business_profile, ) .await .map(|card| Some((card, enums::PaymentMethod::Card)))? diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 376faeb64648..1b23dcab199d 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -78,7 +78,7 @@ use crate::{ }, core::{ errors::{self, StorageErrorExt}, - payment_methods::{transformers as payment_methods, vault}, + payment_methods::{network_tokenization, transformers as payment_methods, vault}, payments::{ helpers, routing::{self, SessionFlowRoutingInput}, @@ -133,6 +133,9 @@ pub async fn create_payment_method( storage_scheme: MerchantStorageScheme, payment_method_billing_address: crypto::OptionalEncryptableValue, card_scheme: Option, + network_token_requestor_reference_id: Option, + network_token_locker_id: Option, + network_token_payment_method_data: crypto::OptionalEncryptableValue, ) -> errors::CustomResult { let db = &*state.store; let customer = db @@ -189,6 +192,9 @@ pub async fn create_payment_method( payment_method_billing_address, updated_by: None, version: domain::consts::API_VERSION, + network_token_requestor_reference_id, + network_token_locker_id, + network_token_payment_method_data, }, storage_scheme, ) @@ -355,6 +361,9 @@ pub async fn get_or_insert_payment_method( req.network_transaction_id.clone(), merchant_account.storage_scheme, None, + None, + None, + None, ) .await } else { @@ -752,6 +761,9 @@ pub async fn skip_locker_call_and_migrate_payment_method( payment_method_billing_address: payment_method_billing_address.map(Into::into), updated_by: None, version: domain::consts::API_VERSION, + network_token_requestor_reference_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, }, merchant_account.storage_scheme, ) @@ -872,6 +884,9 @@ pub async fn get_client_secret_or_add_payment_method( merchant_account.storage_scheme, payment_method_billing_address.map(Into::into), None, + None, + None, + None, ) .await?; @@ -1080,9 +1095,12 @@ pub async fn add_payment_method_data( payment_method_data: Some(pm_data_encrypted.into()), status: Some(enums::PaymentMethodStatus::Active), locker_id: Some(locker_id), + network_token_requestor_reference_id: None, payment_method: req.payment_method, payment_method_issuer: req.payment_method_issuer, payment_method_type: req.payment_method_type, + network_token_locker_id: None, + network_token_payment_method_data: None, }; db.update_payment_method( @@ -1384,6 +1402,9 @@ pub async fn add_payment_method( req.network_transaction_id.clone(), merchant_account.storage_scheme, payment_method_billing_address.map(Into::into), + None, + None, + None, ) .await?; @@ -1424,6 +1445,9 @@ pub async fn insert_payment_method( network_transaction_id: Option, storage_scheme: MerchantStorageScheme, payment_method_billing_address: crypto::OptionalEncryptableValue, + network_token_requestor_reference_id: Option, + network_token_locker_id: Option, + network_token_payment_method_data: crypto::OptionalEncryptableValue, ) -> errors::RouterResult { let pm_card_details = resp .card @@ -1458,6 +1482,9 @@ pub async fn insert_payment_method( card.card_network .map(|card_network| card_network.to_string()) }), + network_token_requestor_reference_id, + network_token_locker_id, + network_token_payment_method_data, ) .await } @@ -4611,6 +4638,9 @@ async fn get_pm_list_context( Some(pm.get_id().clone()), pm.locker_id.clone().or(Some(pm.get_id().clone())), pm.locker_id.clone().unwrap_or(pm.get_id().clone()), + pm.network_token_requestor_reference_id + .clone() + .or(Some(pm.get_id().clone())), ), ), }) @@ -5823,6 +5853,24 @@ pub async fn delete_payment_method( ) .await?; + if let Some(network_token_ref_id) = key.network_token_requestor_reference_id { + let resp = network_tokenization::delete_network_token_from_locker_and_token_service( + &state, + &key.customer_id, + &key.merchant_id, + key.payment_method_id.clone(), + key.network_token_locker_id, + network_token_ref_id, + ) + .await?; + + if resp.status == "Ok" { + logger::info!("Token From locker deleted Successfully!"); + } else { + logger::error!("Error: Deleting Token From Locker!\n{:#?}", resp); + } + } + if response.status == "Ok" { logger::info!("Card From locker deleted Successfully!"); } else { @@ -5860,7 +5908,7 @@ pub async fn delete_payment_method( Ok(services::ApplicationResponse::Json( api::PaymentMethodDeleteResponse { - payment_method_id: key.payment_method_id, + payment_method_id: key.payment_method_id.clone(), deleted: true, }, )) diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs new file mode 100644 index 000000000000..f02bfaa42617 --- /dev/null +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -0,0 +1,699 @@ +use std::fmt::Debug; + +use api_models::{enums as api_enums, payment_methods::PaymentMethodsData}; +use cards::CardNumber; +use common_utils::{ + errors::CustomResult, + ext_traits::{BytesExt, Encode}, + id_type, + metrics::utils::record_operation_time, + request::RequestContent, +}; +use error_stack::ResultExt; +use josekit::jwe; +use masking::{ExposeInterface, Mask, PeekInterface, Secret}; +use serde::{Deserialize, Serialize}; + +use super::transformers::DeleteCardResp; +use crate::{ + core::{errors, payment_methods, payments::helpers}, + headers, logger, + routes::{self, metrics}, + services::{self, encryption}, + settings, + types::{api, domain}, +}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardData { + card_number: CardNumber, + exp_month: Secret, + exp_year: Secret, + card_security_code: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderData { + consent_id: String, + customer_id: id_type::CustomerId, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiPayload { + service: String, + card_data: Secret, //encrypted card data + order_data: OrderData, + key_id: String, + should_send_token: bool, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct CardNetworkTokenResponse { + payload: Secret, //encrypted payload +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardNetworkTokenResponsePayload { + pub card_brand: api_enums::CardNetwork, + pub card_fingerprint: Option>, + pub card_reference: String, + pub correlation_id: String, + pub customer_id: String, + pub par: String, + pub token: CardNumber, + pub token_expiry_month: Secret, + pub token_expiry_year: Secret, + pub token_isin: String, + pub token_last_four: String, + pub token_status: String, +} + +#[derive(Debug, Serialize)] +pub struct GetCardToken { + card_reference: String, + customer_id: id_type::CustomerId, +} +#[derive(Debug, Deserialize)] +pub struct AuthenticationDetails { + cryptogram: Secret, + token: CardNumber, //network token +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenDetails { + exp_month: Secret, + exp_year: Secret, +} + +#[derive(Debug, Deserialize)] +pub struct TokenResponse { + authentication_details: AuthenticationDetails, + network: api_enums::CardNetwork, + token_details: TokenDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteCardToken { + card_reference: String, //network token requestor ref id + customer_id: id_type::CustomerId, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum DeleteNetworkTokenStatus { + Success, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkTokenErrorInfo { + code: String, + developer_message: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct NetworkTokenErrorResponse { + error_message: String, + error_info: NetworkTokenErrorInfo, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct DeleteNetworkTokenResponse { + status: DeleteNetworkTokenStatus, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CheckTokenStatus { + card_reference: String, + customer_id: id_type::CustomerId, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum TokenStatus { + Active, + Inactive, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckTokenStatusResponsePayload { + token_expiry_month: Secret, + token_expiry_year: Secret, + token_status: TokenStatus, +} + +#[derive(Debug, Deserialize)] +pub struct CheckTokenStatusResponse { + payload: CheckTokenStatusResponsePayload, +} + +pub const NETWORK_TOKEN_SERVICE: &str = "NETWORK_TOKEN"; + +pub async fn mk_tokenization_req( + state: &routes::SessionState, + payload_bytes: &[u8], + customer_id: id_type::CustomerId, + tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult<(CardNetworkTokenResponsePayload, Option), errors::NetworkTokenizationError> +{ + let enc_key = tokenization_service.public_key.peek().clone(); + + let key_id = tokenization_service.key_id.clone(); + + let jwt = encryption::encrypt_jwe( + payload_bytes, + enc_key, + services::EncryptionAlgorithm::A128GCM, + Some(key_id.as_str()), + ) + .await + .change_context(errors::NetworkTokenizationError::SaveNetworkTokenFailed) + .attach_printable("Error on jwe encrypt")?; + + let order_data = OrderData { + consent_id: uuid::Uuid::new_v4().to_string(), + customer_id, + }; + + let api_payload = ApiPayload { + service: NETWORK_TOKEN_SERVICE.to_string(), + card_data: Secret::new(jwt), + order_data, + key_id, + should_send_token: true, + }; + + let mut request = services::Request::new( + services::Method::Post, + tokenization_service.generate_token_url.as_str(), + ); + request.add_header(headers::CONTENT_TYPE, "application/json".into()); + request.add_header( + headers::AUTHORIZATION, + tokenization_service + .token_service_api_key + .peek() + .clone() + .into_masked(), + ); + request.add_default_headers(); + + request.set_body(RequestContent::Json(Box::new(api_payload))); + + logger::info!("Request to generate token: {:?}", request); + + let response = services::call_connector_api(state, request, "generate_token") + .await + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed); + + let res = response + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable("Error while receiving response") + .and_then(|inner| match inner { + Err(err_res) => { + let parsed_error: NetworkTokenErrorResponse = err_res + .response + .parse_struct("Card Network Tokenization Response") + .change_context( + errors::NetworkTokenizationError::ResponseDeserializationFailed, + )?; + logger::error!( + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, + "Network tokenization error: {}", + parsed_error.error_message + ); + Err(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable(format!("Response Deserialization Failed: {err_res:?}")) + } + Ok(res) => Ok(res), + }) + .inspect_err(|err| { + logger::error!("Error while deserializing response: {:?}", err); + })?; + + let network_response: CardNetworkTokenResponse = res + .response + .parse_struct("Card Network Tokenization Response") + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + logger::debug!("Network Token Response: {:?}", network_response); //added for debugging, will be removed + + let dec_key = tokenization_service.private_key.peek().clone(); + + let card_network_token_response = services::decrypt_jwe( + network_response.payload.peek(), + services::KeyIdCheck::SkipKeyIdCheck, + dec_key, + jwe::RSA_OAEP_256, + ) + .await + .change_context(errors::NetworkTokenizationError::SaveNetworkTokenFailed) + .attach_printable( + "Failed to decrypt the tokenization response from the tokenization service", + )?; + + let cn_response: CardNetworkTokenResponsePayload = + serde_json::from_str(&card_network_token_response) + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + Ok((cn_response.clone(), Some(cn_response.card_reference))) +} + +pub async fn make_card_network_tokenization_request( + state: &routes::SessionState, + card: &domain::Card, + customer_id: &id_type::CustomerId, +) -> CustomResult<(CardNetworkTokenResponsePayload, Option), errors::NetworkTokenizationError> +{ + let card_data = CardData { + card_number: card.card_number.clone(), + exp_month: card.card_exp_month.clone(), + exp_year: card.card_exp_year.clone(), + card_security_code: card.card_cvc.clone(), + }; + + let payload = card_data + .encode_to_string_of_json() + .and_then(|x| x.encode_to_string_of_json()) + .change_context(errors::NetworkTokenizationError::RequestEncodingFailed)?; + + let payload_bytes = payload.as_bytes(); + if let Some(network_tokenization_service) = &state.conf.network_tokenization_service { + record_operation_time( + async { + mk_tokenization_req( + state, + payload_bytes, + customer_id.clone(), + network_tokenization_service.get_inner(), + ) + .await + .inspect_err( + |e| logger::error!(error=?e, "Error while making tokenization request"), + ) + }, + &metrics::GENERATE_NETWORK_TOKEN_TIME, + &metrics::CONTEXT, + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], + ) + .await + } else { + Err(errors::NetworkTokenizationError::NetworkTokenizationServiceNotConfigured) + .inspect_err(|_| { + logger::error!("Network Tokenization Service not configured"); + }) + .attach_printable("Network Tokenization Service not configured") + } +} + +pub async fn get_network_token( + state: &routes::SessionState, + customer_id: id_type::CustomerId, + network_token_requestor_ref_id: String, + tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult { + let mut request = services::Request::new( + services::Method::Post, + tokenization_service.fetch_token_url.as_str(), + ); + let payload = GetCardToken { + card_reference: network_token_requestor_ref_id, + customer_id, + }; + + request.add_header(headers::CONTENT_TYPE, "application/json".into()); + request.add_header( + headers::AUTHORIZATION, + tokenization_service + .token_service_api_key + .clone() + .peek() + .clone() + .into_masked(), + ); + request.add_default_headers(); + request.set_body(RequestContent::Json(Box::new(payload))); + + logger::info!("Request to fetch network token: {:?}", request); + + // Send the request using `call_connector_api` + let response = services::call_connector_api(state, request, "get network token") + .await + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed); + + let res = response + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable("Error while receiving response") + .and_then(|inner| match inner { + Err(err_res) => { + let parsed_error: NetworkTokenErrorResponse = err_res + .response + .parse_struct("Card Network Tokenization Response") + .change_context( + errors::NetworkTokenizationError::ResponseDeserializationFailed, + )?; + logger::error!( + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, + "Network tokenization error: {}", + parsed_error.error_message + ); + Err(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable(format!("Response Deserialization Failed: {err_res:?}")) + } + Ok(res) => Ok(res), + })?; + + let token_response: TokenResponse = res + .response + .parse_struct("Get Network Token Response") + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + logger::info!("Fetch Network Token Response: {:?}", token_response); + + Ok(token_response) +} + +pub async fn get_token_from_tokenization_service( + state: &routes::SessionState, + network_token_requestor_ref_id: String, + pm_data: &domain::PaymentMethod, +) -> errors::RouterResult { + let token_response = + if let Some(network_tokenization_service) = &state.conf.network_tokenization_service { + record_operation_time( + async { + get_network_token( + state, + pm_data.customer_id.clone(), + network_token_requestor_ref_id, + network_tokenization_service.get_inner(), + ) + .await + .inspect_err( + |e| logger::error!(error=?e, "Error while fetching token from tokenization service") + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Fetch network token failed") + }, + &metrics::FETCH_NETWORK_TOKEN_TIME, + &metrics::CONTEXT, + &[], + ) + .await + } else { + Err(errors::NetworkTokenizationError::NetworkTokenizationServiceNotConfigured) + .inspect_err(|err| { + logger::error!(error=? err); + }) + .change_context(errors::ApiErrorResponse::InternalServerError) + }?; + + let token_decrypted = pm_data + .network_token_payment_method_data + .clone() + .map(|x| x.into_inner().expose()) + .and_then(|v| serde_json::from_value::(v).ok()) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(token) => Some(api::CardDetailFromLocker::from(token)), + _ => None, + }) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to obtain decrypted token object from db")?; + + let network_token_data = domain::NetworkTokenData { + token_number: token_response.authentication_details.token, + token_cryptogram: Some(token_response.authentication_details.cryptogram), + token_exp_month: token_decrypted + .expiry_month + .unwrap_or(token_response.token_details.exp_month), + token_exp_year: token_decrypted + .expiry_year + .unwrap_or(token_response.token_details.exp_year), + nick_name: token_decrypted.card_holder_name, + card_issuer: None, + card_network: Some(token_response.network), + card_type: None, + card_issuing_country: None, + bank_code: None, + }; + Ok(network_token_data) +} + +pub async fn do_status_check_for_network_token( + state: &routes::SessionState, + payment_method_info: &domain::PaymentMethod, +) -> CustomResult<(Option>, Option>), errors::ApiErrorResponse> { + let network_token_data_decrypted = payment_method_info + .network_token_payment_method_data + .clone() + .map(|x| x.into_inner().expose()) + .and_then(|v| serde_json::from_value::(v).ok()) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(token) => Some(api::CardDetailFromLocker::from(token)), + _ => None, + }); + let network_token_requestor_reference_id = payment_method_info + .network_token_requestor_reference_id + .clone(); + if network_token_data_decrypted + .and_then(|token_data| token_data.expiry_month.zip(token_data.expiry_year)) + .and_then(|(exp_month, exp_year)| helpers::validate_card_expiry(&exp_month, &exp_year).ok()) + .is_none() + { + if let Some(ref_id) = network_token_requestor_reference_id { + if let Some(network_tokenization_service) = &state.conf.network_tokenization_service { + let (token_exp_month, token_exp_year) = record_operation_time( + async { + check_token_status_with_tokenization_service( + state, + &payment_method_info.customer_id.clone(), + ref_id, + network_tokenization_service.get_inner(), + ) + .await + .inspect_err( + |e| logger::error!(error=?e, "Error while fetching token from tokenization service") + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Check network token status with tokenization service failed", + ) + }, + &metrics::CHECK_NETWORK_TOKEN_STATUS_TIME, + &metrics::CONTEXT, + &[], + ) + .await?; + Ok((token_exp_month, token_exp_year)) + } else { + Err(errors::NetworkTokenizationError::NetworkTokenizationServiceNotConfigured) + .change_context(errors::ApiErrorResponse::InternalServerError) + .inspect_err(|_| { + logger::error!("Network Tokenization Service not configured"); + }) + } + } else { + Err(errors::NetworkTokenizationError::FetchNetworkTokenFailed) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Check network token status failed")? + } + } else { + Ok((None, None)) + } +} + +pub async fn check_token_status_with_tokenization_service( + state: &routes::SessionState, + customer_id: &id_type::CustomerId, + network_token_requestor_reference_id: String, + tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult<(Option>, Option>), errors::NetworkTokenizationError> +{ + let mut request = services::Request::new( + services::Method::Post, + tokenization_service.check_token_status_url.as_str(), + ); + let payload = CheckTokenStatus { + card_reference: network_token_requestor_reference_id, + customer_id: customer_id.clone(), + }; + + request.add_header(headers::CONTENT_TYPE, "application/json".into()); + request.add_header( + headers::AUTHORIZATION, + tokenization_service + .token_service_api_key + .clone() + .peek() + .clone() + .into_masked(), + ); + request.add_default_headers(); + request.set_body(RequestContent::Json(Box::new(payload))); + + // Send the request using `call_connector_api` + let response = services::call_connector_api(state, request, "Check Network token Status") + .await + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed); + let res = response + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable("Error while receiving response") + .and_then(|inner| match inner { + Err(err_res) => { + let parsed_error: NetworkTokenErrorResponse = err_res + .response + .parse_struct("Delete Network Tokenization Response") + .change_context( + errors::NetworkTokenizationError::ResponseDeserializationFailed, + )?; + logger::error!( + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, + "Network tokenization error: {}", + parsed_error.error_message + ); + Err(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable(format!("Response Deserialization Failed: {err_res:?}")) + } + Ok(res) => Ok(res), + }) + .inspect_err(|err| { + logger::error!("Error while deserializing response: {:?}", err); + })?; + + let check_token_status_response: CheckTokenStatusResponse = res + .response + .parse_struct("Delete Network Tokenization Response") + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + + match check_token_status_response.payload.token_status { + TokenStatus::Active => Ok(( + Some(check_token_status_response.payload.token_expiry_month), + Some(check_token_status_response.payload.token_expiry_year), + )), + TokenStatus::Inactive => Ok((None, None)), + } +} + +pub async fn delete_network_token_from_locker_and_token_service( + state: &routes::SessionState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + payment_method_id: String, + network_token_locker_id: Option, + network_token_requestor_reference_id: String, +) -> errors::RouterResult { + //deleting network token from locker + let resp = payment_methods::cards::delete_card_from_locker( + state, + customer_id, + merchant_id, + network_token_locker_id + .as_ref() + .unwrap_or(&payment_method_id), + ) + .await?; + if let Some(tokenization_service) = &state.conf.network_tokenization_service { + let delete_token_resp = record_operation_time( + async { + delete_network_token_from_tokenization_service( + state, + network_token_requestor_reference_id, + customer_id, + tokenization_service.get_inner(), + ) + .await + }, + &metrics::DELETE_NETWORK_TOKEN_TIME, + &metrics::CONTEXT, + &[], + ) + .await; + match delete_token_resp { + Ok(_) => logger::info!("Token From Tokenization Service deleted Successfully!"), + Err(e) => { + logger::error!(error=?e, "Error while deleting Token From Tokenization Service!") + } + }; + }; + + Ok(resp) +} + +pub async fn delete_network_token_from_tokenization_service( + state: &routes::SessionState, + network_token_requestor_reference_id: String, + customer_id: &id_type::CustomerId, + tokenization_service: &settings::NetworkTokenizationService, +) -> CustomResult { + let mut request = services::Request::new( + services::Method::Post, + tokenization_service.delete_token_url.as_str(), + ); + let payload = DeleteCardToken { + card_reference: network_token_requestor_reference_id, + customer_id: customer_id.clone(), + }; + + request.add_header(headers::CONTENT_TYPE, "application/json".into()); + request.add_header( + headers::AUTHORIZATION, + tokenization_service + .token_service_api_key + .clone() + .peek() + .clone() + .into_masked(), + ); + request.add_default_headers(); + request.set_body(RequestContent::Json(Box::new(payload))); + + logger::info!("Request to delete network token: {:?}", request); + + // Send the request using `call_connector_api` + let response = services::call_connector_api(state, request, "delete network token") + .await + .change_context(errors::NetworkTokenizationError::DeleteNetworkTokenFailed); + let res = response + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable("Error while receiving response") + .and_then(|inner| match inner { + Err(err_res) => { + let parsed_error: NetworkTokenErrorResponse = err_res + .response + .parse_struct("Delete Network Tokenization Response") + .change_context( + errors::NetworkTokenizationError::ResponseDeserializationFailed, + )?; + logger::error!( + error_code = %parsed_error.error_info.code, + developer_message = %parsed_error.error_info.developer_message, + "Network tokenization error: {}", + parsed_error.error_message + ); + Err(errors::NetworkTokenizationError::ResponseDeserializationFailed) + .attach_printable(format!("Response Deserialization Failed: {err_res:?}")) + } + Ok(res) => Ok(res), + }) + .inspect_err(|err| { + logger::error!("Error while deserializing response: {:?}", err); + })?; + + let delete_token_response: DeleteNetworkTokenResponse = res + .response + .parse_struct("Delete Network Tokenization Response") + .change_context(errors::NetworkTokenizationError::ResponseDeserializationFailed)?; + + logger::info!("Delete Network Token Response: {:?}", delete_token_response); + + if delete_token_response.status == DeleteNetworkTokenStatus::Success { + Ok(true) + } else { + Err(errors::NetworkTokenizationError::DeleteNetworkTokenFailed) + .attach_printable("Delete Token at Token service failed") + } +} diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index f01ecffbbb3c..174771414925 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -18,7 +18,7 @@ use crate::{ core::errors::{self, CustomResult}, headers, pii::{prelude::*, Secret}, - services::{api as services, encryption}, + services::{api as services, encryption, EncryptionAlgorithm}, types::{api, domain}, utils::OptionExt, }; @@ -276,10 +276,11 @@ pub async fn mk_basilisk_req( } }; - let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error on jwe encrypt")?; + let jwe_encrypted = + encryption::encrypt_jwe(&payload, public_key, EncryptionAlgorithm::A256GCM, None) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error on jwe encrypt")?; let jwe_payload: Vec<&str> = jwe_encrypted.split('.').collect(); let generate_jwe_body = |payload: Vec<&str>| -> Option { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 3ff5e649f20b..0ec14bd69ede 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -74,7 +74,7 @@ use crate::{ connector::utils::missing_field_err, core::{ errors::{self, CustomResult, RouterResponse, RouterResult}, - payment_methods::cards, + payment_methods::{cards, network_tokenization}, payouts, routing as core_routing, utils, }, db::StorageInterface, @@ -208,7 +208,7 @@ where &validate_result, &key_store, &customer, - Some(&business_profile), + &business_profile, ) .await?; @@ -1581,7 +1581,7 @@ where &merchant_connector_account, key_store, customer, - Some(business_profile), + business_profile, ) .await?; *payment_data = pd; @@ -2614,7 +2614,7 @@ pub async fn get_connector_tokenization_action_when_confirm_true( merchant_connector_account: &helpers::MerchantConnectorAccountType, merchant_key_store: &domain::MerchantKeyStore, customer: &Option, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<(D, TokenizationAction)> where F: Send + Clone, @@ -2628,7 +2628,8 @@ where .and_then(|inner| inner.mandate_reference_id.as_ref()) .map(|mandate_reference| match mandate_reference { api_models::payments::MandateReferenceId::ConnectorMandateId(_) => true, - api_models::payments::MandateReferenceId::NetworkMandateId(_) => false, + api_models::payments::MandateReferenceId::NetworkMandateId(_) + | api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_) => false, }) .unwrap_or(false); @@ -2743,7 +2744,7 @@ pub async fn tokenize_in_router_when_confirm_false_or_external_authentication, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult where F: Send + Clone, @@ -3718,6 +3719,7 @@ where connector_data, mandate_type, business_profile.is_connector_agnostic_mit_enabled, + business_profile.is_network_tokenization_enabled, ) .await; } @@ -3775,6 +3777,7 @@ where connector_data, mandate_type, business_profile.is_connector_agnostic_mit_enabled, + business_profile.is_network_tokenization_enabled, ) .await; } @@ -3804,6 +3807,7 @@ where .await } +#[allow(clippy::too_many_arguments)] pub async fn decide_multiplex_connector_for_normal_or_recurring_payment( state: &SessionState, payment_data: &mut D, @@ -3811,6 +3815,7 @@ pub async fn decide_multiplex_connector_for_normal_or_recurring_payment, mandate_type: Option, is_connector_agnostic_mit_enabled: Option, + is_network_tokenization_enabled: bool, ) -> RouterResult where D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, @@ -3841,59 +3846,189 @@ where let payment_method_info = payment_data .get_payment_method_info() - .get_required_value("payment_method_info")?; - - let connector_mandate_details = &payment_method_info - .connector_mandate_details - .clone() - .map(|details| { - details.parse_value::( - "connector_mandate_details", - ) - }) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize connector mandate details")?; + .get_required_value("payment_method_info")? + .clone(); - let mut connector_choice = None; + //fetch connectors that support ntid flow + let ntid_supported_connectors = &state + .conf + .network_transaction_id_supported_connectors + .connector_list; + //filered connectors list with ntid_supported_connectors + let filtered_ntid_supported_connectors = + filter_ntid_supported_connectors(connectors.clone(), ntid_supported_connectors); + + //fetch connectors that support network tokenization flow + let network_tokenization_supported_connectors = &state + .conf + .network_tokenization_supported_connectors + .connector_list; + //filered connectors list with ntid_supported_connectors and network_tokenization_supported_connectors + let filtered_nt_supported_connectors = filter_network_tokenization_supported_connectors( + filtered_ntid_supported_connectors, + network_tokenization_supported_connectors, + ); - for connector_data in connectors { - let merchant_connector_id = connector_data - .merchant_connector_id - .as_ref() - .ok_or(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to find the merchant connector id")?; + let action_type = decide_action_type( + state, + is_connector_agnostic_mit_enabled, + is_network_tokenization_enabled, + &payment_method_info, + filtered_nt_supported_connectors.clone(), + ) + .await; - if is_network_transaction_id_flow( - state, - is_connector_agnostic_mit_enabled, - connector_data.connector_name, - payment_method_info, - ) { - logger::info!("using network_transaction_id for MIT flow"); - let network_transaction_id = payment_method_info - .network_transaction_id - .as_ref() - .ok_or(errors::ApiErrorResponse::InternalServerError)?; + match action_type { + Some(ActionType::NetworkTokenWithNetworkTransactionId(nt_data)) => { + logger::info!( + "using network_tokenization with network_transaction_id for MIT flow" + ); let mandate_reference_id = - Some(payments_api::MandateReferenceId::NetworkMandateId( - network_transaction_id.to_string(), + Some(payments_api::MandateReferenceId::NetworkTokenWithNTI( + payments_api::NetworkTokenWithNTIRef { + network_transaction_id: nt_data.network_transaction_id.to_string(), + token_exp_month: nt_data.token_exp_month, + token_exp_year: nt_data.token_exp_year, + }, )); + let chosen_connector_data = filtered_nt_supported_connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .attach_printable( + "no eligible connector found for token-based MIT payment", + )?; - connector_choice = Some((connector_data, mandate_reference_id.clone())); - break; - } else if connector_mandate_details - .clone() - .map(|connector_mandate_details| { - connector_mandate_details.contains_key(merchant_connector_id) - }) - .unwrap_or(false) - { - if let Some(merchant_connector_id) = - connector_data.merchant_connector_id.as_ref() - { - if let Some(mandate_reference_record) = connector_mandate_details.clone() + routing_data.routed_through = + Some(chosen_connector_data.connector_name.to_string()); + + routing_data + .merchant_connector_id + .clone_from(&chosen_connector_data.merchant_connector_id); + + payment_data.set_mandate_id(payments_api::MandateIds { + mandate_id: None, + mandate_reference_id, + }); + + Ok(ConnectorCallType::PreDetermined( + chosen_connector_data.clone(), + )) + } + None => { + decide_connector_for_normal_or_recurring_payment( + state, + payment_data, + routing_data, + connectors, + is_connector_agnostic_mit_enabled, + &payment_method_info, + ) + .await + } + } + } + ( + None, + None, + Some(RecurringDetails::ProcessorPaymentToken(_token)), + Some(true), + Some(api::MandateTransactionType::RecurringMandateTransaction), + ) => { + if let Some(connector) = connectors.first() { + routing_data.routed_through = Some(connector.connector_name.clone().to_string()); + routing_data + .merchant_connector_id + .clone_from(&connector.merchant_connector_id); + Ok(ConnectorCallType::PreDetermined(api::ConnectorData { + connector: connector.connector.clone(), + connector_name: connector.connector_name, + get_token: connector.get_token.clone(), + merchant_connector_id: connector.merchant_connector_id.clone(), + })) + } else { + logger::error!("no eligible connector found for the ppt_mandate payment"); + Err(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration.into()) + } + } + + _ => { + helpers::override_setup_future_usage_to_on_session(&*state.store, payment_data).await?; + + let first_choice = connectors + .first() + .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .attach_printable("no eligible connector found for payment")? + .clone(); + + routing_data.routed_through = Some(first_choice.connector_name.to_string()); + + routing_data.merchant_connector_id = first_choice.merchant_connector_id; + + Ok(ConnectorCallType::Retryable(connectors)) + } + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn decide_connector_for_normal_or_recurring_payment( + state: &SessionState, + payment_data: &mut D, + routing_data: &mut storage::RoutingData, + connectors: Vec, + is_connector_agnostic_mit_enabled: Option, + payment_method_info: &domain::PaymentMethod, +) -> RouterResult +where + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ + let connector_mandate_details = &payment_method_info + .connector_mandate_details + .clone() + .map(|details| { + details.parse_value::("connector_mandate_details") + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize connector mandate details")?; + + let mut connector_choice = None; + + for connector_data in connectors { + let merchant_connector_id = connector_data + .merchant_connector_id + .as_ref() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to find the merchant connector id")?; + if is_network_transaction_id_flow( + state, + is_connector_agnostic_mit_enabled, + connector_data.connector_name, + payment_method_info, + ) { + logger::info!("using network_transaction_id for MIT flow"); + let network_transaction_id = payment_method_info + .network_transaction_id + .as_ref() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch the network transaction id")?; + + let mandate_reference_id = Some(payments_api::MandateReferenceId::NetworkMandateId( + network_transaction_id.to_string(), + )); + + connector_choice = Some((connector_data, mandate_reference_id.clone())); + break; + } else if connector_mandate_details + .clone() + .map(|connector_mandate_details| { + connector_mandate_details.contains_key(merchant_connector_id) + }) + .unwrap_or(false) + { + logger::info!("using connector_mandate_id for MIT flow"); + if let Some(merchant_connector_id) = connector_data.merchant_connector_id.as_ref() { + if let Some(mandate_reference_record) = connector_mandate_details.clone() .get_required_value("connector_mandate_details") .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) .attach_printable("no eligible connector found for token-based MIT flow since there were no connector mandate details")? @@ -3935,69 +4070,102 @@ where connector_choice = Some((connector_data, mandate_reference_id.clone())); break; } - } - } else { - continue; - } } + } else { + continue; + } + } - let (chosen_connector_data, mandate_reference_id) = connector_choice - .get_required_value("connector_choice") - .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) - .attach_printable("no eligible connector found for token-based MIT payment")?; + let (chosen_connector_data, mandate_reference_id) = connector_choice + .get_required_value("connector_choice") + .change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) + .attach_printable("no eligible connector found for token-based MIT payment")?; - routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string()); + routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string()); - routing_data - .merchant_connector_id - .clone_from(&chosen_connector_data.merchant_connector_id); + routing_data + .merchant_connector_id + .clone_from(&chosen_connector_data.merchant_connector_id); - payment_data.set_mandate_id(payments_api::MandateIds { - mandate_id: None, - mandate_reference_id, - }); + payment_data.set_mandate_id(payments_api::MandateIds { + mandate_id: None, + mandate_reference_id, + }); - Ok(ConnectorCallType::PreDetermined(chosen_connector_data)) - } - ( - None, - None, - Some(RecurringDetails::ProcessorPaymentToken(_token)), - Some(true), - Some(api::MandateTransactionType::RecurringMandateTransaction), - ) => { - if let Some(connector) = connectors.first() { - routing_data.routed_through = Some(connector.connector_name.clone().to_string()); - routing_data - .merchant_connector_id - .clone_from(&connector.merchant_connector_id); - Ok(ConnectorCallType::PreDetermined(api::ConnectorData { - connector: connector.connector.clone(), - connector_name: connector.connector_name, - get_token: connector.get_token.clone(), - merchant_connector_id: connector.merchant_connector_id.clone(), - })) - } else { - logger::error!("no eligible connector found for the ppt_mandate payment"); - Err(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration.into()) - } - } + Ok(ConnectorCallType::PreDetermined(chosen_connector_data)) +} - _ => { - helpers::override_setup_future_usage_to_on_session(&*state.store, payment_data).await?; +pub fn filter_ntid_supported_connectors( + connectors: Vec, + ntid_supported_connectors: &HashSet, +) -> Vec { + connectors + .into_iter() + .filter(|data| ntid_supported_connectors.contains(&data.connector_name)) + .collect() +} - let first_choice = connectors - .first() - .ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration) - .attach_printable("no eligible connector found for payment")? - .clone(); +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] +pub struct NetworkTokenExpiry { + pub token_exp_month: Option>, + pub token_exp_year: Option>, +} - routing_data.routed_through = Some(first_choice.connector_name.to_string()); +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] +pub struct NTWithNTIRef { + pub network_transaction_id: String, + pub token_exp_month: Option>, + pub token_exp_year: Option>, +} - routing_data.merchant_connector_id = first_choice.merchant_connector_id; +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] +pub enum ActionType { + NetworkTokenWithNetworkTransactionId(NTWithNTIRef), +} - Ok(ConnectorCallType::Retryable(connectors)) +pub fn filter_network_tokenization_supported_connectors( + connectors: Vec, + network_tokenization_supported_connectors: &HashSet, +) -> Vec { + connectors + .into_iter() + .filter(|data| network_tokenization_supported_connectors.contains(&data.connector_name)) + .collect() +} + +pub async fn decide_action_type( + state: &SessionState, + is_connector_agnostic_mit_enabled: Option, + is_network_tokenization_enabled: bool, + payment_method_info: &domain::PaymentMethod, + filtered_nt_supported_connectors: Vec, //network tokenization supported connectors +) -> Option { + match ( + is_network_token_with_network_transaction_id_flow( + is_connector_agnostic_mit_enabled, + is_network_tokenization_enabled, + payment_method_info, + ), + !filtered_nt_supported_connectors.is_empty(), + ) { + (IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id), true) => { + if let Ok((token_exp_month, token_exp_year)) = + network_tokenization::do_status_check_for_network_token(state, payment_method_info) + .await + { + Some(ActionType::NetworkTokenWithNetworkTransactionId( + NTWithNTIRef { + token_exp_month, + token_exp_year, + network_transaction_id, + }, + )) + } else { + None + } } + (IsNtWithNtiFlow::NtWithNtiSupported(_), false) + | (IsNtWithNtiFlow::NTWithNTINotSupported, _) => None, } } @@ -4018,6 +4186,39 @@ pub fn is_network_transaction_id_flow( && payment_method_info.network_transaction_id.is_some() } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] +pub enum IsNtWithNtiFlow { + NtWithNtiSupported(String), //Network token with Network transaction id supported flow + NTWithNTINotSupported, //Network token with Network transaction id not supported +} + +pub fn is_network_token_with_network_transaction_id_flow( + is_connector_agnostic_mit_enabled: Option, + is_network_tokenization_enabled: bool, + payment_method_info: &domain::PaymentMethod, +) -> IsNtWithNtiFlow { + match ( + is_connector_agnostic_mit_enabled, + is_network_tokenization_enabled, + payment_method_info.payment_method, + payment_method_info.network_transaction_id.clone(), + payment_method_info.network_token_locker_id.is_some(), + payment_method_info + .network_token_requestor_reference_id + .is_some(), + ) { + ( + Some(true), + true, + Some(storage_enums::PaymentMethod::Card), + Some(network_transaction_id), + true, + true, + ) => IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id), + _ => IsNtWithNtiFlow::NTWithNTINotSupported, + } +} + pub fn should_add_task_to_process_tracker>( payment_data: &D, ) -> bool { @@ -4220,6 +4421,7 @@ where connector_data, mandate_type, business_profile.is_connector_agnostic_mit_enabled, + business_profile.is_network_tokenization_enabled, ) .await } @@ -4444,12 +4646,22 @@ pub async fn payment_external_authentication( .await .to_not_found_response(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error while fetching authentication record")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(key_manager_state, &key_store, profile_id) + .await + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.get_string_repr().to_owned(), + })?; + let payment_method_details = helpers::get_payment_method_details_from_payment_token( &state, &payment_attempt, &payment_intent, &key_store, storage_scheme, + &business_profile, ) .await? .ok_or(errors::ApiErrorResponse::InternalServerError) @@ -4475,14 +4687,6 @@ pub async fn payment_external_authentication( let webhook_url = helpers::create_webhook_url(&state.base_url, merchant_id, &authentication_connector); - let business_profile = state - .store - .find_business_profile_by_profile_id(key_manager_state, &key_store, profile_id) - .await - .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { - id: profile_id.get_string_repr().to_owned(), - })?; - let authentication_details = business_profile .authentication_connector_details .clone() diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 6cb4532d6179..2af3c52d7583 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -66,7 +66,7 @@ use crate::{ payment_methods::{ self, cards::{self}, - vault, + network_tokenization, vault, }, payments, pm_auth::retrieve_payment_method_from_auth_service, @@ -1781,6 +1781,7 @@ pub async fn retrieve_payment_method_with_temporary_token( }) } +#[allow(clippy::too_many_arguments)] pub async fn retrieve_card_with_permanent_token( state: &SessionState, locker_id: &str, @@ -1789,6 +1790,9 @@ pub async fn retrieve_card_with_permanent_token( card_token_data: Option<&domain::CardToken>, _merchant_key_store: &domain::MerchantKeyStore, _storage_scheme: enums::MerchantStorageScheme, + mandate_id: Option, + payment_method_info: Option, + business_profile: &domain::BusinessProfile, ) -> RouterResult { let customer_id = payment_intent .customer_id @@ -1797,11 +1801,148 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::UnprocessableEntity { message: "no customer id provided for the payment".to_string(), })?; - let card = - cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, locker_id) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch card information from the permanent locker")?; + + if !business_profile.is_network_tokenization_enabled { + fetch_card_details_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + locker_id, + card_token_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker") + } else { + match (payment_method_info, mandate_id) { + (None, _) => Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Payment method data is not present"), + (Some(ref pm_data), None) => { + // Regular (non-mandate) Payment flow + if let Some(token_ref) = pm_data.network_token_requestor_reference_id.clone() { + match network_tokenization::get_token_from_tokenization_service( + state, token_ref, pm_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "failed to fetch network token data from tokenization service", + ) { + Ok(network_token_data) => { + Ok(domain::PaymentMethodData::NetworkToken(network_token_data)) + } + Err(err) => { + logger::info!("Failed to fetch network token data from tokenization service {err:?}"); + logger::info!("Falling back to fetch card details from locker"); + fetch_card_details_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + locker_id, + card_token_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "failed to fetch card information from the permanent locker", + ) + } + } + } else { + fetch_card_details_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + locker_id, + card_token_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker") + } + } + (Some(ref pm_data), Some(mandate_ids)) => { + // Mandate Payment flow + match mandate_ids.mandate_reference_id { + Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI( + nt_data, + )) => { + { + if let Some(network_token_locker_id) = + pm_data.network_token_locker_id.as_ref() + { + let mut token_data = cards::get_card_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + network_token_locker_id, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "failed to fetch network token information from the permanent locker", + )?; + let expiry = nt_data.token_exp_month.zip(nt_data.token_exp_year); + if let Some((exp_month, exp_year)) = expiry { + token_data.card_exp_month = exp_month; + token_data.card_exp_year = exp_year; + } + let network_token_data = domain::NetworkTokenData { + token_number: token_data.card_number, + token_cryptogram: None, + token_exp_month: token_data.card_exp_month, + token_exp_year: token_data.card_exp_year, + nick_name: token_data.nick_name.map(masking::Secret::new), + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }; + Ok(domain::PaymentMethodData::NetworkToken(network_token_data)) + } else { + // Mandate but network token locker id is not present + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Network token locker id is not present") + } + } + } + + Some(api_models::payments::MandateReferenceId::NetworkMandateId(_)) => { + fetch_card_details_from_locker( + state, + customer_id, + &payment_intent.merchant_id, + locker_id, + card_token_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "failed to fetch card information from the permanent locker", + ) + } + + Some(api_models::payments::MandateReferenceId::ConnectorMandateId(_)) + | None => Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Payment method data is not present"), + } + } + } + } +} + +pub async fn fetch_card_details_from_locker( + state: &SessionState, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, + locker_id: &str, + card_token_data: Option<&domain::CardToken>, +) -> RouterResult { + let card = cards::get_card_from_locker(state, customer_id, merchant_id, locker_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?; // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked // from payment_method_data.card_token object @@ -1842,7 +1983,6 @@ pub async fn retrieve_card_with_permanent_token( card_issuing_country: None, bank_code: None, }; - Ok(domain::PaymentMethodData::Card(api_card.into())) } @@ -1949,7 +2089,7 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( merchant_key_store: &domain::MerchantKeyStore, customer: &Option, storage_scheme: common_enums::enums::MerchantStorageScheme, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<( BoxedOperation<'a, F, R, D>, Option, @@ -1986,15 +2126,21 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( .locker_id .clone() .unwrap_or(payment_method_info.get_id().clone()), + network_token_locker_id: payment_method_info + .network_token_requestor_reference_id + .clone() + .or(Some(payment_method_info.get_id().clone())), })); } } } + let mandate_id = payment_data.mandate_id.clone(); + // TODO: Handle case where payment method and token both are present in request properly. let (payment_method, pm_id) = match (&request, payment_data.token_data.as_ref()) { (_, Some(hyperswitch_token)) => { - let pm_data = payment_methods::retrieve_payment_method_with_token( + let pm_data = Box::pin(payment_methods::retrieve_payment_method_with_token( state, merchant_key_store, hyperswitch_token, @@ -2002,7 +2148,10 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( card_token_data.as_ref(), customer, storage_scheme, - ) + mandate_id, + payment_data.payment_method_info.clone(), + business_profile, + )) .await; let payment_method_details = pm_data.attach_printable("in 'make_pm_data'")?; @@ -2028,7 +2177,7 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( &payment_data.payment_intent, &payment_data.payment_attempt, merchant_key_store, - business_profile, + Some(business_profile), ) .await?; @@ -4927,6 +5076,7 @@ pub async fn get_payment_method_details_from_payment_token( payment_intent: &PaymentIntent, key_store: &domain::MerchantKeyStore, storage_scheme: enums::MerchantStorageScheme, + business_profile: &domain::BusinessProfile, ) -> RouterResult> { let hyperswitch_token = if let Some(token) = payment_attempt.payment_token.clone() { let redis_conn = state @@ -5010,6 +5160,9 @@ pub async fn get_payment_method_details_from_payment_token( None, key_store, storage_scheme, + None, + None, + business_profile, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))), @@ -5025,6 +5178,9 @@ pub async fn get_payment_method_details_from_payment_token( None, key_store, storage_scheme, + None, + None, + business_profile, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))), diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index e4692075914b..4a38fac8522b 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -148,7 +148,7 @@ pub trait Domain: Send + Sync { storage_scheme: enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, customer: &Option, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<( BoxedOperation<'a, F, R, D>, Option, @@ -371,7 +371,7 @@ where _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRetrieveRequest, D>, Option, @@ -465,7 +465,7 @@ where _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCaptureRequest, D>, Option, @@ -570,7 +570,7 @@ where _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsCancelRequest, D>, Option, @@ -634,7 +634,7 @@ where _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( BoxedOperation<'a, F, api::PaymentsRejectRequest, D>, Option, diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 00eb0a3bc3e2..0032aa2f6b36 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -400,13 +400,13 @@ impl Domain> for Comple storage_scheme: storage_enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, customer: &Option, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<( CompleteAuthorizeOperation<'a, F>, Option, Option, )> { - let (op, payment_method_data, pm_id) = helpers::make_pm_data( + let (op, payment_method_data, pm_id) = Box::pin(helpers::make_pm_data( Box::new(self), state, payment_data, @@ -414,7 +414,7 @@ impl Domain> for Comple customer, storage_scheme, business_profile, - ) + )) .await?; Ok((op, payment_method_data, pm_id)) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 90158793f960..850563807531 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -776,13 +776,13 @@ impl Domain> for Paymen storage_scheme: storage_enums::MerchantStorageScheme, key_store: &domain::MerchantKeyStore, customer: &Option, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentConfirmOperation<'a, F>, Option, Option, )> { - let (op, payment_method_data, pm_id) = helpers::make_pm_data( + let (op, payment_method_data, pm_id) = Box::pin(helpers::make_pm_data( Box::new(self), state, payment_data, @@ -790,7 +790,7 @@ impl Domain> for Paymen customer, storage_scheme, business_profile, - ) + )) .await?; utils::when(payment_method_data.is_none(), || { @@ -987,7 +987,7 @@ impl Domain> for Paymen }; let encrypted_payload = - services::encrypt_jwe(&card_data, merchant_config.public_key.peek()) + services::encrypt_jwe(&card_data, merchant_config.public_key.peek(), services::EncryptionAlgorithm::A256GCM, None) .await .map_err(|err| { logger::error!(jwe_encryption_err=?err,"Error while JWE encrypting extended card info") diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index d66565e84e58..fb6bb0447783 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -688,13 +688,13 @@ impl Domain> for Paymen storage_scheme: enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, customer: &Option, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentCreateOperation<'a, F>, Option, Option, )> { - helpers::make_pm_data( + Box::pin(helpers::make_pm_data( Box::new(self), state, payment_data, @@ -702,7 +702,7 @@ impl Domain> for Paymen customer, storage_scheme, business_profile, - ) + )) .await } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 5471ae355f5c..24ee0697ac30 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -872,16 +872,42 @@ async fn payment_response_update_tracker( locale: &Option, ) -> RouterResult> { // Update additional payment data with the payment method response that we received from connector - let additional_payment_method_data = - update_additional_payment_data_with_connector_response_pm_data( - payment_data.payment_attempt.payment_method_data.clone(), - router_data - .connector_response - .as_ref() - .and_then(|connector_response| { - connector_response.additional_payment_method_data.clone() - }), - )?; + let additional_payment_method_data = match payment_data.payment_method_data.clone() { + Some(payment_method_data) => match payment_method_data { + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Card(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::CardRedirect(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::Wallet(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::PayLater(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::BankRedirect(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::BankDebit(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::BankTransfer(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::Crypto(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::MandatePayment + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::Reward + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::RealTimePayment( + _, + ) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::Upi(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::Voucher(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::GiftCard(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::CardToken(_) + | hyperswitch_domain_models::payment_method_data::PaymentMethodData::OpenBanking(_) => { + update_additional_payment_data_with_connector_response_pm_data( + payment_data.payment_attempt.payment_method_data.clone(), + router_data + .connector_response + .as_ref() + .and_then(|connector_response| { + connector_response.additional_payment_method_data.clone() + }), + )? + } + hyperswitch_domain_models::payment_method_data::PaymentMethodData::NetworkToken(_) => { + payment_data.payment_attempt.payment_method_data.clone() + } + }, + None => None, + }; router_data.payment_method_status.and_then(|status| { payment_data diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index e58dd8855fd9..f12cd71cdb0c 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -330,7 +330,7 @@ where _storage_scheme: storage_enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentSessionOperation<'b, F>, Option, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 6895c9e7b06a..cd98d448c361 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -302,7 +302,7 @@ where storage_scheme: storage_enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, customer: &Option, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentSessionOperation<'a, F>, Option, @@ -315,7 +315,7 @@ where .map(|connector_name| connector_name == *"bluesnap".to_string()) .unwrap_or(false) { - helpers::make_pm_data( + Box::pin(helpers::make_pm_data( Box::new(self), state, payment_data, @@ -323,7 +323,7 @@ where customer, storage_scheme, business_profile, - ) + )) .await } else { Ok((Box::new(self), None, None)) diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d7ca01e66610..7c2ef7a10f45 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -93,7 +93,7 @@ impl Domain> for Paymen _storage_scheme: enums::MerchantStorageScheme, _merchant_key_store: &domain::MerchantKeyStore, _customer: &Option, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentStatusOperation<'a, F, api::PaymentsRequest>, Option, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 484bec14f484..a1bfb47fd929 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -628,13 +628,13 @@ impl Domain> for Paymen storage_scheme: storage_enums::MerchantStorageScheme, merchant_key_store: &domain::MerchantKeyStore, customer: &Option, - business_profile: Option<&domain::BusinessProfile>, + business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentUpdateOperation<'a, F>, Option, Option, )> { - helpers::make_pm_data( + Box::pin(helpers::make_pm_data( Box::new(self), state, payment_data, @@ -642,7 +642,7 @@ impl Domain> for Paymen customer, storage_scheme, business_profile, - ) + )) .await } diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index 29ae20a30496..c95fa22a039f 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -335,7 +335,7 @@ impl Domain, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentIncrementalAuthorizationOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/operations/tax_calculation.rs b/crates/router/src/core/payments/operations/tax_calculation.rs index 1df836124402..64252762e6fb 100644 --- a/crates/router/src/core/payments/operations/tax_calculation.rs +++ b/crates/router/src/core/payments/operations/tax_calculation.rs @@ -329,7 +329,7 @@ impl Domain, - _business_profile: Option<&domain::BusinessProfile>, + _business_profile: &domain::BusinessProfile, ) -> RouterResult<( PaymentSessionUpdateOperation<'a, F>, Option, diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 609d5592ffd5..46e02e6b1a88 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -21,7 +21,7 @@ use crate::{ core::{ errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt}, mandate, - payment_methods::{self, cards::create_encrypted_data}, + payment_methods::{self, cards::create_encrypted_data, network_tokenization}, payments, }, logger, @@ -199,22 +199,65 @@ where .await?; let customer_id = customer_id.to_owned().get_required_value("customer_id")?; let merchant_id = merchant_account.get_id(); - let (mut resp, duplication_check) = if !state.conf.locker.locker_enabled { - skip_saving_card_in_locker( + let is_network_tokenization_enabled = + business_profile.is_network_tokenization_enabled; + let ( + (mut resp, duplication_check, network_token_requestor_ref_id), + network_token_resp, + ) = if !state.conf.locker.locker_enabled { + let (res, dc) = skip_saving_card_in_locker( merchant_account, payment_method_create_request.to_owned(), ) - .await? + .await?; + ((res, dc, None), None) } else { pm_status = Some(common_enums::PaymentMethodStatus::from( save_payment_method_data.attempt_status, )); - Box::pin(save_in_locker( + let (res, dc) = Box::pin(save_in_locker( state, merchant_account, payment_method_create_request.to_owned(), )) - .await? + .await?; + + if is_network_tokenization_enabled { + let pm_data = &save_payment_method_data.request.get_payment_method_data(); + match pm_data { + domain::PaymentMethodData::Card(card) => { + let ( + network_token_resp, + _network_token_duplication_check, //the duplication check is discarded, since each card has only one token, handling card duplication check will be suffice + network_token_requestor_ref_id, + ) = Box::pin(save_network_token_in_locker( + state, + merchant_account, + card, + payment_method_create_request.clone(), + )) + .await?; + + ( + (res, dc, network_token_requestor_ref_id), + network_token_resp, + ) + } + _ => ((res, dc, None), None), //network_token_resp is None in case of other payment methods + } + } else { + ((res, dc, None), None) + } + }; + let network_token_locker_id = match network_token_resp { + Some(ref token_resp) => { + if network_token_requestor_ref_id.is_some() { + Some(token_resp.payment_method_id.clone()) + } else { + None + } + } + None => None, }; let pm_card_details = resp.card.as_ref().map(|card| { @@ -229,6 +272,24 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt payment method data")?; + let pm_network_token_data_encrypted: Option< + Encryptable>, + > = match network_token_resp { + Some(token_resp) => { + let pm_token_details = token_resp.card.as_ref().map(|card| { + PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())) + }); + + pm_token_details + .async_map(|pm_card| create_encrypted_data(state, key_store, pm_card)) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt payment method data")? + } + None => None, + }; + let encrypted_payment_method_billing_address: Option< Encryptable>, > = payment_method_billing_address @@ -346,6 +407,9 @@ where card.card_network .map(|card_network| card_network.to_string()) }), + network_token_requestor_ref_id, + network_token_locker_id, + pm_network_token_data_encrypted.map(Into::into), ) .await } else { @@ -449,6 +513,9 @@ where card_network.to_string() }) }), + network_token_requestor_ref_id, + network_token_locker_id, + pm_network_token_data_encrypted.map(Into::into), ) .await } else { @@ -651,6 +718,9 @@ where card.card_network .map(|card_network| card_network.to_string()) }), + network_token_requestor_ref_id, + network_token_locker_id, + pm_network_token_data_encrypted.map(Into::into), ) .await?; }; @@ -861,6 +931,94 @@ pub async fn save_in_locker( todo!() } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn save_network_token_in_locker( + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _card_data: &domain::Card, + _payment_method_request: api::PaymentMethodCreate, +) -> RouterResult<( + Option, + Option, + Option, +)> { + todo!() +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +pub async fn save_network_token_in_locker( + state: &SessionState, + merchant_account: &domain::MerchantAccount, + card_data: &domain::Card, + payment_method_request: api::PaymentMethodCreate, +) -> RouterResult<( + Option, + Option, + Option, +)> { + let customer_id = payment_method_request + .customer_id + .clone() + .get_required_value("customer_id")?; + let network_tokenization_supported_card_networks = &state + .conf + .network_tokenization_supported_card_networks + .card_networks; + + if card_data + .card_network + .as_ref() + .filter(|cn| network_tokenization_supported_card_networks.contains(cn)) + .is_some() + { + match network_tokenization::make_card_network_tokenization_request( + state, + card_data, + &customer_id, + ) + .await + { + Ok((token_response, network_token_requestor_ref_id)) => { + // Only proceed if the tokenization was successful + let card_data = api::CardDetail { + card_number: token_response.token.clone(), + card_exp_month: token_response.token_expiry_month.clone(), + card_exp_year: token_response.token_expiry_year.clone(), + card_holder_name: None, + nick_name: None, + card_issuing_country: None, + card_network: Some(token_response.card_brand.clone()), + card_issuer: None, + card_type: None, + }; + + let (res, dc) = Box::pin(payment_methods::cards::add_card_to_locker( + state, + payment_method_request, + &card_data, + &customer_id, + merchant_account, + None, + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Network Token Failed")?; + + Ok((Some(res), dc, network_token_requestor_ref_id)) + } + Err(err) => { + logger::error!("Failed to tokenize card: {:?}", err); + Ok((None, None, None)) //None will be returned in case of error when calling network tokenization service + } + } + } else { + Ok((None, None, None)) //None will be returned in case of unsupported card network. + } +} + pub fn create_payment_method_metadata( metadata: Option<&pii::SecretSerdeValue>, connector_token: Option<(String, String)>, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 853706327942..a48aed1fb2bd 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -712,14 +712,37 @@ where .map(ToString::to_string) .unwrap_or("".to_owned()); let additional_payment_method_data: Option = - payment_attempt - .payment_method_data - .clone() - .map(|data| data.parse_value("payment_method_data")) - .transpose() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data", - })?; + match payment_data.get_payment_method_data(){ + Some(payment_method_data) => match payment_method_data{ + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Card(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::CardRedirect(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Wallet(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::PayLater(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::BankRedirect(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::BankDebit(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::BankTransfer(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Crypto(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::MandatePayment | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Reward | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::RealTimePayment(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Upi(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Voucher(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::GiftCard(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::CardToken(_) | + hyperswitch_domain_models::payment_method_data::PaymentMethodData::OpenBanking(_) => {payment_attempt + .payment_method_data + .clone() + .map(|data| data.parse_value("payment_method_data")) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + })?}, + hyperswitch_domain_models::payment_method_data::PaymentMethodData::NetworkToken(_) => None, + } + None => None + + }; + let surcharge_details = payment_attempt .surcharge_amount diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 035c5c139a88..913f8ec7618d 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -537,6 +537,9 @@ pub async fn save_payout_data_to_locker( merchant_account.storage_scheme, None, None, + None, + None, + None, ) .await?; } diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index bb47a54b1256..f948d7faaf97 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -527,6 +527,9 @@ async fn store_bank_details_in_payment_methods( payment_method_billing_address: None, updated_by: None, version: domain::consts::API_VERSION, + network_token_requestor_reference_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, }; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -551,6 +554,9 @@ async fn store_bank_details_in_payment_methods( updated_by: None, locker_fingerprint_id: None, version: domain::consts::API_VERSION, + network_token_requestor_reference_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, }; new_entries.push(pm_new); diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index 970ba5bd2de5..f3f1cd7cda0f 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -134,3 +134,9 @@ counter_metric!(ACCESS_TOKEN_CACHE_MISS, GLOBAL_METER); // A counter to indicate the integrity check failures counter_metric!(INTEGRITY_CHECK_FAILED, GLOBAL_METER); + +// Network Tokenization metrics +histogram_metric!(GENERATE_NETWORK_TOKEN_TIME, GLOBAL_METER); +histogram_metric!(FETCH_NETWORK_TOKEN_TIME, GLOBAL_METER); +histogram_metric!(DELETE_NETWORK_TOKEN_TIME, GLOBAL_METER); +histogram_metric!(CHECK_NETWORK_TOKEN_STATUS_TIME, GLOBAL_METER); diff --git a/crates/router/src/services/encryption.rs b/crates/router/src/services/encryption.rs index dc7e5db079a6..8321eaff969d 100644 --- a/crates/router/src/services/encryption.rs +++ b/crates/router/src/services/encryption.rs @@ -26,15 +26,26 @@ pub struct JweBody { pub encrypted_key: String, } +#[derive(Debug, Eq, PartialEq, Copy, Clone, strum::AsRefStr, strum::Display)] +pub enum EncryptionAlgorithm { + A128GCM, + A256GCM, +} + pub async fn encrypt_jwe( payload: &[u8], public_key: impl AsRef<[u8]>, + algorithm: EncryptionAlgorithm, + key_id: Option<&str>, ) -> CustomResult { let alg = jwe::RSA_OAEP_256; - let enc = "A256GCM"; let mut src_header = jwe::JweHeader::new(); - src_header.set_content_encryption(enc); + let enc_str = algorithm.as_ref(); + src_header.set_content_encryption(enc_str); src_header.set_token_type("JWT"); + if let Some(key_id) = key_id { + src_header.set_key_id(key_id); + } let encrypter = alg .encrypter_from_pem(public_key) .change_context(errors::EncryptionError) @@ -208,9 +219,14 @@ VuY3OeNxi+dC2r7HppP3O/MJ4gX/RJJfSrcaGP8/Ke1W5+jE97Qy #[actix_rt::test] async fn test_jwe() { - let jwt = encrypt_jwe("request_payload".as_bytes(), ENCRYPTION_KEY) - .await - .unwrap(); + let jwt = encrypt_jwe( + "request_payload".as_bytes(), + ENCRYPTION_KEY, + EncryptionAlgorithm::A256GCM, + None, + ) + .await + .unwrap(); let alg = jwe::RSA_OAEP_256; let payload = decrypt_jwe(&jwt, KeyIdCheck::SkipKeyIdCheck, DECRYPTION_KEY, alg) .await diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 9d90ef530aed..549c544f30f6 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -163,6 +163,7 @@ impl ForeignTryFrom for BusinessProfileResponse { outgoing_webhook_custom_http_headers, tax_connector_id: item.tax_connector_id, is_tax_connector_enabled: item.is_tax_connector_enabled, + is_network_tokenization_enabled: item.is_network_tokenization_enabled, }) } } @@ -228,6 +229,7 @@ impl ForeignTryFrom for BusinessProfileResponse { order_fulfillment_time_origin: item.order_fulfillment_time_origin, tax_connector_id: item.tax_connector_id, is_tax_connector_enabled: item.is_tax_connector_enabled, + is_network_tokenization_enabled: item.is_network_tokenization_enabled, }) } } @@ -348,6 +350,7 @@ pub async fn create_business_profile_from_merchant_account( tax_connector_id: request.tax_connector_id, is_tax_connector_enabled: request.is_tax_connector_enabled, dynamic_routing_algorithm: None, + is_network_tokenization_enabled: request.is_network_tokenization_enabled, }, )) } diff --git a/crates/router/src/types/domain/payments.rs b/crates/router/src/types/domain/payments.rs index bfd7b54f5563..53bc138c6492 100644 --- a/crates/router/src/types/domain/payments.rs +++ b/crates/router/src/types/domain/payments.rs @@ -4,11 +4,11 @@ pub use hyperswitch_domain_models::payment_method_data::{ CardToken, CashappQr, CryptoData, GcashRedirection, GiftCardData, GiftCardDetails, GoPayRedirection, GooglePayPaymentMethodInfo, GooglePayRedirectData, GooglePayThirdPartySdkData, GooglePayWalletData, GpayTokenizationData, IndomaretVoucherData, - KakaoPayRedirection, MbWayRedirection, MifinityData, OpenBankingData, PayLaterData, - PaymentMethodData, RealTimePaymentData, SamsungPayWalletData, SepaAndBacsBillingDetails, - SwishQrData, TokenizedBankDebitValue1, TokenizedBankDebitValue2, TokenizedBankRedirectValue1, - TokenizedBankRedirectValue2, TokenizedBankTransferValue1, TokenizedBankTransferValue2, - TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, - TouchNGoRedirection, UpiCollectData, UpiData, UpiIntentData, VoucherData, WalletData, - WeChatPayQr, + KakaoPayRedirection, MbWayRedirection, MifinityData, NetworkTokenData, OpenBankingData, + PayLaterData, PaymentMethodData, RealTimePaymentData, SamsungPayWalletData, + SepaAndBacsBillingDetails, SwishQrData, TokenizedBankDebitValue1, TokenizedBankDebitValue2, + TokenizedBankRedirectValue1, TokenizedBankRedirectValue2, TokenizedBankTransferValue1, + TokenizedBankTransferValue2, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, + TokenizedWalletValue2, TouchNGoRedirection, UpiCollectData, UpiData, UpiIntentData, + VoucherData, WalletData, WeChatPayQr, }; diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 0e7bdb14d4a0..5bbfd8483cd8 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -24,6 +24,7 @@ pub struct CardTokenData { pub payment_method_id: Option, pub locker_id: Option, pub token: String, + pub network_token_locker_id: Option, } #[derive(Debug, Clone, serde::Serialize, Default, serde::Deserialize)] @@ -61,11 +62,13 @@ impl PaymentTokenData { payment_method_id: Option, locker_id: Option, token: String, + network_token_locker_id: Option, ) -> Self { Self::PermanentCard(CardTokenData { payment_method_id, locker_id, token, + network_token_locker_id, }) } diff --git a/migrations/2024-09-12-112019_add_is_network_tokenization_enabled_in_business_profile/down.sql b/migrations/2024-09-12-112019_add_is_network_tokenization_enabled_in_business_profile/down.sql new file mode 100644 index 000000000000..2d15e286236f --- /dev/null +++ b/migrations/2024-09-12-112019_add_is_network_tokenization_enabled_in_business_profile/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile DROP COLUMN IF EXISTS is_network_tokenization_enabled; \ No newline at end of file diff --git a/migrations/2024-09-12-112019_add_is_network_tokenization_enabled_in_business_profile/up.sql b/migrations/2024-09-12-112019_add_is_network_tokenization_enabled_in_business_profile/up.sql new file mode 100644 index 000000000000..c45ae434ac3d --- /dev/null +++ b/migrations/2024-09-12-112019_add_is_network_tokenization_enabled_in_business_profile/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS is_network_tokenization_enabled BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/migrations/2024-09-12-123315_add_network_token_locker_id_and_network_token_payment_method_data_and_network_token_ref_id_in_payment_methods/down.sql b/migrations/2024-09-12-123315_add_network_token_locker_id_and_network_token_payment_method_data_and_network_token_ref_id_in_payment_methods/down.sql new file mode 100644 index 000000000000..f550984cf047 --- /dev/null +++ b/migrations/2024-09-12-123315_add_network_token_locker_id_and_network_token_payment_method_data_and_network_token_ref_id_in_payment_methods/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_methods DROP COLUMN IF EXISTS network_token_requestor_reference_id; + +ALTER TABLE payment_methods DROP COLUMN IF EXISTS network_token_locker_id; + +ALTER TABLE payment_methods DROP COLUMN IF EXISTS network_token_payment_method_data; \ No newline at end of file diff --git a/migrations/2024-09-12-123315_add_network_token_locker_id_and_network_token_payment_method_data_and_network_token_ref_id_in_payment_methods/up.sql b/migrations/2024-09-12-123315_add_network_token_locker_id_and_network_token_payment_method_data_and_network_token_ref_id_in_payment_methods/up.sql new file mode 100644 index 000000000000..598afc9247a6 --- /dev/null +++ b/migrations/2024-09-12-123315_add_network_token_locker_id_and_network_token_payment_method_data_and_network_token_ref_id_in_payment_methods/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS network_token_requestor_reference_id VARCHAR(128) DEFAULT NULL; + +ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS network_token_locker_id VARCHAR(64) DEFAULT NULL; + +ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS network_token_payment_method_data BYTEA DEFAULT NULL; \ No newline at end of file