diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index 9e0b54dd7ebf..c4314c522f9e 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -335,4 +335,6 @@ jobs: - name: Cargo hack_v2 shell: bash + env: + GH_TOKEN: ${{ github.token }} run: just hack_v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 34af7055950b..6f827380b78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.08.06.0 + +### Features + +- **core:** Accept profile_id in merchant_account, connectors and customers core functions ([#5505](https://github.com/juspay/hyperswitch/pull/5505)) ([`a3e01bb`](https://github.com/juspay/hyperswitch/commit/a3e01bb4ae5893f639f3846ccb73adcca6b25ee0)) +- **router:** Add accept language header middleware ([#5500](https://github.com/juspay/hyperswitch/pull/5500)) ([`ec5f9de`](https://github.com/juspay/hyperswitch/commit/ec5f9de0cbfecd584cc200e26b13a37aebe97f16)) + +### Bug Fixes + +- **connector:** Fixed status mapping for Plaid ([#5525](https://github.com/juspay/hyperswitch/pull/5525)) ([`4364630`](https://github.com/juspay/hyperswitch/commit/4364630d6ffbce43bef0947a0150ce255a43751a)) + +### Refactors + +- **auth:** Pass `profile_id` from the auth to core functions ([#5520](https://github.com/juspay/hyperswitch/pull/5520)) ([`53b5551`](https://github.com/juspay/hyperswitch/commit/53b5551df7e7a04bb26532591180608542c33c3a)) +- **cypress:** Pass `connector_type` externally ([#5522](https://github.com/juspay/hyperswitch/pull/5522)) ([`7743255`](https://github.com/juspay/hyperswitch/commit/7743255cd8618ea290e714f2268c07bac4cfa88d)) +- **router:** Refactor merchant_connector update v2 flow ([#5484](https://github.com/juspay/hyperswitch/pull/5484)) ([`9e358e4`](https://github.com/juspay/hyperswitch/commit/9e358e4f7bac86fbf433bbb727e41e4a9acf29ea)) +- **routing:** Refactor api v2 routes for deactivating and retrieving the routing config ([#5478](https://github.com/juspay/hyperswitch/pull/5478)) ([`3fea00c`](https://github.com/juspay/hyperswitch/commit/3fea00c43ee597c9b786da6636e245cb848cdb97)) + +**Full Changelog:** [`2024.08.05.0...2024.08.06.0`](https://github.com/juspay/hyperswitch/compare/2024.08.05.0...2024.08.06.0) + +- - - + ## 2024.08.05.0 ### Bug Fixes diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index cc53e69cb768..1e62053dde18 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -7059,7 +7059,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "routing_algorithm": { @@ -7221,7 +7221,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "routing_algorithm": { @@ -7949,6 +7949,7 @@ "trustpay", "tsys", "volt", + "wellsfargo", "wise", "worldline", "worldpay", @@ -11761,7 +11762,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "publishable_key": { @@ -11893,7 +11894,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "test_mode": { @@ -12003,7 +12004,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true } } @@ -12122,7 +12123,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "test_mode": { @@ -12289,7 +12290,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "test_mode": { @@ -12440,7 +12441,7 @@ }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "test_mode": { @@ -20134,6 +20135,7 @@ "trustpay", "tsys", "volt", + "wellsfargo", "wise", "worldline", "worldpay", diff --git a/config/config.example.toml b/config/config.example.toml index 7cbd65513033..d01d67cc6847 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -194,8 +194,7 @@ bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" boku.base_url = "https://$-api4-stage.boku.com" -braintree.base_url = "https://api.sandbox.braintreegateway.com/" -braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" @@ -543,8 +542,6 @@ adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamal [bank_config.online_banking_thailand] adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" -[multiple_api_version_supported_connectors] -supported_connectors = "braintree" [applepay_decrypt_keys] apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" # Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index be2d22d92a04..16b715b7e732 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -34,8 +34,7 @@ bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" boku.base_url = "https://$-api4-stage.boku.com" -braintree.base_url = "https://api.sandbox.braintreegateway.com/" -braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" @@ -158,8 +157,6 @@ card.debit = { connector_list = "cybersource" } # Update Mandate sup [network_transaction_id_supported_connectors] connector_list = "stripe,adyen,cybersource" -[multiple_api_version_supported_connectors] -supported_connectors = "braintree" [payouts] payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility diff --git a/config/deployments/production.toml b/config/deployments/production.toml index f26568848715..6bf81d32424f 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -38,8 +38,7 @@ bitpay.base_url = "https://bitpay.com" bluesnap.base_url = "https://ws.bluesnap.com/" bluesnap.secondary_base_url = "https://pay.bluesnap.com/" boku.base_url = "https://country-api4-stage.boku.com" -braintree.base_url = "https://api.sandbox.braintreegateway.com/" -braintree.secondary_base_url = "https://payments.braintree-api.com/graphql" +braintree.base_url = "https://payments.braintree-api.com/graphql" cashtocode.base_url = "https://cluster14.api.cashtocode.com" checkout.base_url = "https://api.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" @@ -155,8 +154,6 @@ bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # M card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card card.debit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card -[multiple_api_version_supported_connectors] -supported_connectors = "braintree" [payouts] payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 146a9bb87f81..e1e346a8253d 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -38,8 +38,7 @@ bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" boku.base_url = "https://$-api4-stage.boku.com" -braintree.base_url = "https://api.sandbox.braintreegateway.com/" -braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" @@ -158,8 +157,6 @@ card.debit = { connector_list = "cybersource" } # Update Mandate sup [network_transaction_id_supported_connectors] connector_list = "stripe,adyen,cybersource" -[multiple_api_version_supported_connectors] -supported_connectors = "braintree" [payouts] payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility diff --git a/config/development.toml b/config/development.toml index 8888048ccfae..1b82a36dc0fc 100644 --- a/config/development.toml +++ b/config/development.toml @@ -195,8 +195,7 @@ bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" boku.base_url = "https://$-api4-stage.boku.com" -braintree.base_url = "https://api.sandbox.braintreegateway.com/" -braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" @@ -579,8 +578,6 @@ merchant_ids_send_payment_id_as_connector_request_id = [] [payouts] payout_eligibility = true -[multiple_api_version_supported_connectors] -supported_connectors = "braintree" [applepay_decrypt_keys] apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index fa5ccaac0a60..d893756b7b61 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -123,8 +123,7 @@ bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" boku.base_url = "https://$-api4-stage.boku.com" -braintree.base_url = "https://api.sandbox.braintreegateway.com/" -braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" @@ -441,8 +440,6 @@ connector_list = "stripe,adyen,cybersource" connector_list = "gocardless,stax,stripe" payout_connector_list = "stripe,wise" -[multiple_api_version_supported_connectors] -supported_connectors = "braintree" [payment_method_auth] redis_expiry = 900 diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index f829d2e48fec..e2b9e2d3a55c 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -24,6 +24,7 @@ customer_v2 = [] merchant_account_v2 = [] merchant_connector_account_v2 = [] payment_v2 = [] +payment_methods_v2 = [] routing_v2 = [] [dependencies] diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index feee34863900..dc0d3248b25d 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -251,6 +251,11 @@ pub struct MerchantAccountMetadata { #[serde(flatten)] pub data: Option, } + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct MerchantAccountUpdate { @@ -301,7 +306,7 @@ pub struct MerchantAccountUpdate { #[schema(default = false, example = true)] pub redirect_to_merchant_with_http_post: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, @@ -330,6 +335,110 @@ pub struct MerchantAccountUpdate { pub pm_collect_link_config: Option, } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] +impl MerchantAccountUpdate { + pub fn get_primary_details_as_value( + &self, + ) -> CustomResult, errors::ParsingError> { + self.primary_business_details + .as_ref() + .map(|primary_business_details| primary_business_details.encode_to_value()) + .transpose() + } + + pub fn get_pm_link_config_as_value( + &self, + ) -> CustomResult, errors::ParsingError> { + self.pm_collect_link_config + .as_ref() + .map(|pm_collect_link_config| pm_collect_link_config.encode_to_value()) + .transpose() + } + + pub fn get_merchant_details_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.merchant_details + .as_ref() + .map(|merchant_details| merchant_details.encode_to_value().map(Secret::new)) + .transpose() + } + + pub fn get_metadata_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.metadata + .as_ref() + .map(|metadata| metadata.encode_to_value().map(Secret::new)) + .transpose() + } + + pub fn get_webhook_details_as_value( + &self, + ) -> CustomResult, errors::ParsingError> { + self.webhook_details + .as_ref() + .map(|webhook_details| webhook_details.encode_to_value()) + .transpose() + } + + pub fn parse_routing_algorithm(&self) -> CustomResult<(), errors::ParsingError> { + match self.routing_algorithm { + Some(ref routing_algorithm) => { + let _: routing::RoutingAlgorithm = + routing_algorithm.clone().parse_value("RoutingAlgorithm")?; + Ok(()) + } + None => Ok(()), + } + } + + // Get the enable payment response hash as a boolean, where the default value is true + pub fn get_enable_payment_response_hash(&self) -> bool { + self.enable_payment_response_hash.unwrap_or(true) + } +} + +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] +#[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MerchantAccountUpdate { + /// Name of the Merchant Account + #[schema(example = "NewAge Retailer")] + pub merchant_name: Option, + + /// Details about the merchant + pub merchant_details: Option, + + /// Metadata is useful for storing additional, unstructured information on an object. + #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] + pub metadata: Option, +} + +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] +impl MerchantAccountUpdate { + pub fn get_merchant_details_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.merchant_details + .as_ref() + .map(|merchant_details| merchant_details.encode_to_value().map(Secret::new)) + .transpose() + } + + pub fn get_metadata_as_secret( + &self, + ) -> CustomResult, errors::ParsingError> { + self.metadata + .as_ref() + .map(|metadata| metadata.encode_to_value().map(Secret::new)) + .transpose() + } +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "merchant_account_v2") @@ -452,9 +561,6 @@ pub struct MerchantAccountResponse { #[schema(value_type = String, max_length = 64, min_length = 1, example = "org_q98uSGAYbjEwqs0mJwnz")] pub organization_id: id_type::OrganizationId, - /// A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false - pub is_recon_enabled: bool, - /// Used to indicate the status of the recon module for a merchant account #[schema(value_type = ReconStatus, example = "not_requested")] pub recon_status: api_enums::ReconStatus, @@ -626,7 +732,7 @@ pub struct MerchantConnectorCreate { }))] pub connector_webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -756,7 +862,7 @@ pub struct MerchantConnectorCreate { }))] pub connector_webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -991,7 +1097,7 @@ pub struct MerchantConnectorResponse { }))] pub connector_webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -1098,7 +1204,7 @@ pub struct MerchantConnectorResponse { }))] pub connector_webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -1210,7 +1316,7 @@ pub struct MerchantConnectorListResponse { ]))] pub payment_methods_enabled: Option>, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -1319,7 +1425,7 @@ pub struct MerchantConnectorListResponse { ]))] pub payment_methods_enabled: Option>, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -1414,7 +1520,7 @@ pub struct MerchantConnectorUpdate { }))] pub connector_webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -1712,7 +1818,7 @@ pub struct MerchantConnectorDetails { /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: pii::SecretSerdeValue, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, } @@ -1742,7 +1848,7 @@ pub struct BusinessProfileCreate { /// Webhook related details pub webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, @@ -1834,7 +1940,7 @@ pub struct BusinessProfileResponse { /// Webhook related details pub webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, @@ -1923,7 +2029,7 @@ pub struct BusinessProfileUpdate { /// Webhook related details pub webhook_details: Option, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, diff --git a/crates/api_models/src/conditional_configs.rs b/crates/api_models/src/conditional_configs.rs index 46bad3e9e862..555e7bd955f0 100644 --- a/crates/api_models/src/conditional_configs.rs +++ b/crates/api_models/src/conditional_configs.rs @@ -97,6 +97,7 @@ pub struct DecisionManagerRequest { pub name: Option, pub program: Option>, } + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(untagged)] pub enum DecisionManager { diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 00c2f84f8927..eed29b1aa0d9 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -135,7 +135,7 @@ pub enum Connector { // Tsys, Tsys, Volt, - // Wellsfargo, + Wellsfargo, Wise, Worldline, Worldpay, @@ -255,7 +255,7 @@ impl Connector { | Self::Trustpay | Self::Tsys | Self::Volt - // | Self::Wellsfargo + | Self::Wellsfargo | Self::Wise | Self::Worldline | Self::Worldpay diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index ad2e2546f37c..589dcd8d10b3 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -1,12 +1,19 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] +use crate::payment_methods::CustomerPaymentMethodsListResponse; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::payment_methods::CustomerPaymentMethodsListResponse; use crate::{ payment_methods::{ - CustomerDefaultPaymentMethodResponse, CustomerPaymentMethodsListResponse, - DefaultPaymentMethod, ListCountriesCurrenciesRequest, ListCountriesCurrenciesResponse, - PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, - PaymentMethodCollectLinkResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, - PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, + CustomerDefaultPaymentMethodResponse, DefaultPaymentMethod, ListCountriesCurrenciesRequest, + ListCountriesCurrenciesResponse, PaymentMethodCollectLinkRenderRequest, + PaymentMethodCollectLinkRequest, PaymentMethodCollectLinkResponse, + PaymentMethodDeleteResponse, PaymentMethodListRequest, PaymentMethodListResponse, + PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints, diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 3f1df4b8e963..c90031dc0772 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -994,6 +994,19 @@ impl serde::Serialize for PaymentMethodList { } } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct CustomerPaymentMethodsListResponse { + /// List of payment methods for customer + pub customer_payment_methods: Vec, + /// Returns whether a customer id is not tied to a payment intent (only when the request is made against a client secret) + pub is_guest_customer: Option, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Serialize, ToSchema)] pub struct CustomerPaymentMethodsListResponse { /// List of payment methods for customer @@ -1028,6 +1041,97 @@ pub struct CustomerDefaultPaymentMethodResponse { pub payment_method_type: Option, } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, ToSchema)] +pub struct CustomerPaymentMethod { + /// Token for payment method in temporary card locker which gets refreshed often + #[schema(example = "7ebf443f-a050-4067-84e5-e6f6d4800aef")] + pub payment_token: Option, + /// The unique identifier of the customer. + #[schema(example = "pm_iouuy468iyuowqs")] + pub payment_method_id: String, + + /// The unique identifier of the customer. + #[schema(value_type = String, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] + pub customer_id: id_type::CustomerId, + + /// The type of payment method use for the payment. + #[schema(value_type = PaymentMethod,example = "card")] + pub payment_method: api_enums::PaymentMethod, + + /// This is a sub-category of payment method. + #[schema(value_type = Option,example = "credit_card")] + pub payment_method_type: Option, + + /// The name of the bank/ provider issuing the payment method to the end user + #[schema(example = "Citibank")] + pub payment_method_issuer: Option, + + /// A standard code representing the issuer of payment method + #[schema(value_type = Option,example = "jp_applepay")] + pub payment_method_issuer_code: Option, + + /// Indicates whether the payment method is eligible for recurring payments + #[schema(example = true)] + pub recurring_enabled: bool, + + /// Indicates whether the payment method is eligible for installment payments + #[schema(example = true)] + pub installment_payment_enabled: bool, + + /// Type of payment experience enabled with the connector + #[schema(value_type = Option>,example = json!(["redirect_to_url"]))] + pub payment_experience: Option>, + + /// PaymentMethod Data from locker + pub payment_method_data: Option, + + /// Masked bank details from PM auth services + #[schema(example = json!({"mask": "0000"}))] + pub bank: Option, + + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] + pub metadata: Option, + + /// A timestamp (ISO 8601 code) that determines when the customer was created + #[schema(value_type = Option,example = "2023-01-18T11:04:09.922Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created: Option, + + /// Surcharge details for this saved card + pub surcharge_details: Option, + + /// Whether this payment method requires CVV to be collected + #[schema(example = true)] + pub requires_cvv: bool, + + /// A timestamp (ISO 8601 code) that determines when the payment method was last used + #[schema(value_type = Option,example = "2024-02-24T11:04:09.922Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub last_used_at: Option, + /// Indicates if the payment method has been set to default or not + #[schema(example = true)] + pub default_payment_method_set: bool, + + /// The billing details of the payment method + #[schema(value_type = Option
)] + pub billing: Option, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum PaymentMethodListData { + Card(CardDetailFromLocker), + #[cfg(feature = "payouts")] + Bank(payouts::Bank), +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] #[derive(Debug, Clone, serde::Serialize, ToSchema)] pub struct CustomerPaymentMethod { /// Token for payment method in temporary card locker which gets refreshed often diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index e3017a740451..8c9fc2f06c9e 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -248,7 +248,7 @@ pub enum RoutableConnectors { // Tsys, Tsys, Volt, - // Wellsfargo, + Wellsfargo, Wise, Worldline, Worldpay, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index bc781e05294f..ed36dbca4972 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -117,6 +117,7 @@ pub struct ConnectorTomlConfig { pub bank_transfer: Option>, pub bank_redirect: Option>, pub bank_debit: Option>, + pub open_banking: Option>, pub pay_later: Option>, pub wallet: Option>, pub crypto: Option>, @@ -199,6 +200,7 @@ pub struct ConnectorConfig { pub netcetera: Option, pub tsys: Option, pub volt: Option, + pub wellsfargo: Option, #[cfg(feature = "payouts")] pub wise_payout: Option, pub worldline: Option, @@ -331,6 +333,7 @@ impl ConnectorConfig { Connector::Threedsecureio => Ok(connector_data.threedsecureio), Connector::Tsys => Ok(connector_data.tsys), Connector::Volt => Ok(connector_data.volt), + Connector::Wellsfargo => Ok(connector_data.wellsfargo), Connector::Wise => Err("Use get_payout_connector_config".to_string()), Connector::Worldline => Ok(connector_data.worldline), Connector::Worldpay => Ok(connector_data.worldpay), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 331d9ba744d2..95b22f8f77f1 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -2513,6 +2513,13 @@ type="Text" api_key="Login" key1="Trankey" +[plaid] +[[plaid.open_banking]] + payment_method_type = "open_banking_pis" +[plaid.connector_auth.BodyKey] +api_key="client_id" +key1="secret" + [powertranz] [[powertranz.credit]] payment_method_type = "Mastercard" @@ -3794,4 +3801,50 @@ key1="Public Api Key" payment_method_type = "UnionPay" [datatrans.connector_auth.BodyKey] api_key = "Passcode" -key1 = "datatrans MerchantId" \ No newline at end of file +key1 = "datatrans MerchantId" + +[wellsfargo] +[[wellsfargo.credit]] + payment_method_type = "Mastercard" +[[wellsfargo.credit]] + payment_method_type = "Visa" +[[wellsfargo.credit]] + payment_method_type = "Interac" +[[wellsfargo.credit]] + payment_method_type = "AmericanExpress" +[[wellsfargo.credit]] + payment_method_type = "JCB" +[[wellsfargo.credit]] + payment_method_type = "DinersClub" +[[wellsfargo.credit]] + payment_method_type = "Discover" +[[wellsfargo.credit]] + payment_method_type = "CartesBancaires" +[[wellsfargo.credit]] + payment_method_type = "UnionPay" +[[wellsfargo.debit]] + payment_method_type = "Mastercard" +[[wellsfargo.debit]] + payment_method_type = "Visa" +[[wellsfargo.debit]] + payment_method_type = "Interac" +[[wellsfargo.debit]] + payment_method_type = "AmericanExpress" +[[wellsfargo.debit]] + payment_method_type = "JCB" +[[wellsfargo.debit]] + payment_method_type = "DinersClub" +[[wellsfargo.debit]] + payment_method_type = "Discover" +[[wellsfargo.debit]] + payment_method_type = "CartesBancaires" +[[wellsfargo.debit]] + payment_method_type = "UnionPay" +[[wellsfargo.wallet]] + payment_method_type = "apple_pay" +[[wellsfargo.wallet]] + payment_method_type = "google_pay" +[wellsfargo.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index cfdc2e4b6350..a2c118f572b8 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -2436,6 +2436,13 @@ key1="Payme Public Key" merchant_secret="Payme Client Secret" additional_secret="Payme Client Key" +[plaid] +[[plaid.open_banking]] + payment_method_type = "open_banking_pis" +[plaid.connector_auth.BodyKey] +api_key="client_id" +key1="secret" + [powertranz] [[powertranz.credit]] payment_method_type = "Mastercard" @@ -2712,4 +2719,50 @@ key1="Public Api Key" payment_method_type = "UnionPay" [datatrans.connector_auth.BodyKey] api_key = "Passcode" -key1 = "datatrans MerchantId" \ No newline at end of file +key1 = "datatrans MerchantId" + +[wellsfargo] +[[wellsfargo.credit]] + payment_method_type = "Mastercard" +[[wellsfargo.credit]] + payment_method_type = "Visa" +[[wellsfargo.credit]] + payment_method_type = "Interac" +[[wellsfargo.credit]] + payment_method_type = "AmericanExpress" +[[wellsfargo.credit]] + payment_method_type = "JCB" +[[wellsfargo.credit]] + payment_method_type = "DinersClub" +[[wellsfargo.credit]] + payment_method_type = "Discover" +[[wellsfargo.credit]] + payment_method_type = "CartesBancaires" +[[wellsfargo.credit]] + payment_method_type = "UnionPay" +[[wellsfargo.debit]] + payment_method_type = "Mastercard" +[[wellsfargo.debit]] + payment_method_type = "Visa" +[[wellsfargo.debit]] + payment_method_type = "Interac" +[[wellsfargo.debit]] + payment_method_type = "AmericanExpress" +[[wellsfargo.debit]] + payment_method_type = "JCB" +[[wellsfargo.debit]] + payment_method_type = "DinersClub" +[[wellsfargo.debit]] + payment_method_type = "Discover" +[[wellsfargo.debit]] + payment_method_type = "CartesBancaires" +[[wellsfargo.debit]] + payment_method_type = "UnionPay" +[[wellsfargo.wallet]] + payment_method_type = "apple_pay" +[[wellsfargo.wallet]] + payment_method_type = "google_pay" +[wellsfargo.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index ca5f6fb72e6d..1c4b61db41cc 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -2508,6 +2508,13 @@ type="Text" api_key="Login" key1="Trankey" +[plaid] +[[plaid.open_banking]] + payment_method_type = "open_banking_pis" +[plaid.connector_auth.BodyKey] +api_key="client_id" +key1="secret" + [powertranz] [[powertranz.credit]] payment_method_type = "Mastercard" @@ -3787,4 +3794,50 @@ key1="Public Api Key" payment_method_type = "UnionPay" [datatrans.connector_auth.BodyKey] api_key = "Passcode" -key1 = "datatrans MerchantId" \ No newline at end of file +key1 = "datatrans MerchantId" + +[wellsfargo] +[[wellsfargo.credit]] + payment_method_type = "Mastercard" +[[wellsfargo.credit]] + payment_method_type = "Visa" +[[wellsfargo.credit]] + payment_method_type = "Interac" +[[wellsfargo.credit]] + payment_method_type = "AmericanExpress" +[[wellsfargo.credit]] + payment_method_type = "JCB" +[[wellsfargo.credit]] + payment_method_type = "DinersClub" +[[wellsfargo.credit]] + payment_method_type = "Discover" +[[wellsfargo.credit]] + payment_method_type = "CartesBancaires" +[[wellsfargo.credit]] + payment_method_type = "UnionPay" +[[wellsfargo.debit]] + payment_method_type = "Mastercard" +[[wellsfargo.debit]] + payment_method_type = "Visa" +[[wellsfargo.debit]] + payment_method_type = "Interac" +[[wellsfargo.debit]] + payment_method_type = "AmericanExpress" +[[wellsfargo.debit]] + payment_method_type = "JCB" +[[wellsfargo.debit]] + payment_method_type = "DinersClub" +[[wellsfargo.debit]] + payment_method_type = "Discover" +[[wellsfargo.debit]] + payment_method_type = "CartesBancaires" +[[wellsfargo.debit]] + payment_method_type = "UnionPay" +[[wellsfargo.wallet]] + payment_method_type = "apple_pay" +[[wellsfargo.wallet]] + payment_method_type = "google_pay" +[wellsfargo.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" \ No newline at end of file diff --git a/crates/diesel_models/src/merchant_account.rs b/crates/diesel_models/src/merchant_account.rs index dd830f204348..0f87748d2769 100644 --- a/crates/diesel_models/src/merchant_account.rs +++ b/crates/diesel_models/src/merchant_account.rs @@ -1,6 +1,7 @@ use common_utils::{encryption::Encryption, pii}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use crate::enums as storage_enums; #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "merchant_account_v2") @@ -8,7 +9,6 @@ use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use crate::schema::merchant_account; #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] use crate::schema_v2::merchant_account; -use crate::{business_profile::WebhookDetails, enums as storage_enums}; /// Note: The order of fields in the struct is important. /// This should be in the same order as the fields in the schema.rs file, otherwise the code will not compile @@ -36,7 +36,7 @@ pub struct MerchantAccount { pub redirect_to_merchant_with_http_post: bool, pub merchant_name: Option, pub merchant_details: Option, - pub webhook_details: Option, + pub webhook_details: Option, pub sub_merchants_enabled: Option, pub parent_merchant_id: Option, pub publishable_key: Option, @@ -70,7 +70,7 @@ pub struct MerchantAccountSetter { pub redirect_to_merchant_with_http_post: bool, pub merchant_name: Option, pub merchant_details: Option, - pub webhook_details: Option, + pub webhook_details: Option, pub sub_merchants_enabled: Option, pub parent_merchant_id: Option, pub publishable_key: Option, @@ -146,32 +146,18 @@ impl From for MerchantAccount { )] #[diesel(table_name = merchant_account, primary_key(id), check_for_backend(diesel::pg::Pg))] pub struct MerchantAccount { - pub return_url: Option, - pub enable_payment_response_hash: bool, - pub payment_response_hash_key: Option, - pub redirect_to_merchant_with_http_post: bool, pub merchant_name: Option, pub merchant_details: Option, - pub webhook_details: Option, - pub sub_merchants_enabled: Option, - pub parent_merchant_id: Option, pub publishable_key: Option, pub storage_scheme: storage_enums::MerchantStorageScheme, - pub locker_id: Option, pub metadata: Option, pub routing_algorithm: Option, - pub primary_business_details: serde_json::Value, - pub intent_fulfillment_time: Option, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, pub frm_routing_algorithm: Option, pub payout_routing_algorithm: Option, pub organization_id: common_utils::id_type::OrganizationId, - pub is_recon_enabled: bool, - pub default_profile: Option, pub recon_status: storage_enums::ReconStatus, - pub payment_link_config: Option, - pub pm_collect_link_config: Option, pub id: common_utils::id_type::MerchantId, } @@ -180,32 +166,18 @@ impl From for MerchantAccount { fn from(item: MerchantAccountSetter) -> Self { Self { id: item.id, - return_url: item.return_url, - enable_payment_response_hash: item.enable_payment_response_hash, - payment_response_hash_key: item.payment_response_hash_key, - redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, merchant_name: item.merchant_name, merchant_details: item.merchant_details, - webhook_details: item.webhook_details, - sub_merchants_enabled: item.sub_merchants_enabled, - parent_merchant_id: item.parent_merchant_id, publishable_key: item.publishable_key, storage_scheme: item.storage_scheme, - locker_id: item.locker_id, metadata: item.metadata, - routing_algorithm: item.routing_algorithm, - primary_business_details: item.primary_business_details, - intent_fulfillment_time: item.intent_fulfillment_time, created_at: item.created_at, modified_at: item.modified_at, frm_routing_algorithm: item.frm_routing_algorithm, + routing_algorithm: item.routing_algorithm, payout_routing_algorithm: item.payout_routing_algorithm, organization_id: item.organization_id, - is_recon_enabled: item.is_recon_enabled, - default_profile: item.default_profile, recon_status: item.recon_status, - payment_link_config: item.payment_link_config, - pm_collect_link_config: item.pm_collect_link_config, } } } @@ -213,32 +185,18 @@ impl From for MerchantAccount { #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] pub struct MerchantAccountSetter { pub id: common_utils::id_type::MerchantId, - pub return_url: Option, - pub enable_payment_response_hash: bool, - pub payment_response_hash_key: Option, - pub redirect_to_merchant_with_http_post: bool, pub merchant_name: Option, pub merchant_details: Option, - pub webhook_details: Option, - pub sub_merchants_enabled: Option, - pub parent_merchant_id: Option, pub publishable_key: Option, pub storage_scheme: storage_enums::MerchantStorageScheme, - pub locker_id: Option, pub metadata: Option, pub routing_algorithm: Option, - pub primary_business_details: serde_json::Value, - pub intent_fulfillment_time: Option, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, pub frm_routing_algorithm: Option, pub payout_routing_algorithm: Option, pub organization_id: common_utils::id_type::OrganizationId, - pub is_recon_enabled: bool, - pub default_profile: Option, pub recon_status: storage_enums::ReconStatus, - pub payment_link_config: Option, - pub pm_collect_link_config: Option, } impl MerchantAccount { @@ -268,7 +226,7 @@ pub struct MerchantAccountNew { pub merchant_name: Option, pub merchant_details: Option, pub return_url: Option, - pub webhook_details: Option, + pub webhook_details: Option, pub sub_merchants_enabled: Option, pub parent_merchant_id: Option, pub enable_payment_response_hash: Option, @@ -298,39 +256,46 @@ pub struct MerchantAccountNew { pub struct MerchantAccountNew { pub merchant_name: Option, pub merchant_details: Option, - pub return_url: Option, - pub webhook_details: Option, - pub sub_merchants_enabled: Option, - pub parent_merchant_id: Option, - pub enable_payment_response_hash: Option, - pub payment_response_hash_key: Option, - pub redirect_to_merchant_with_http_post: Option, pub publishable_key: Option, - pub locker_id: Option, pub metadata: Option, pub routing_algorithm: Option, - pub primary_business_details: serde_json::Value, - pub intent_fulfillment_time: Option, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, pub frm_routing_algorithm: Option, pub payout_routing_algorithm: Option, pub organization_id: common_utils::id_type::OrganizationId, - pub is_recon_enabled: bool, - pub default_profile: Option, pub recon_status: storage_enums::ReconStatus, - pub payment_link_config: Option, - pub pm_collect_link_config: Option, pub id: common_utils::id_type::MerchantId, } -#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = merchant_account)] +pub struct MerchantAccountUpdateInternal { + pub merchant_name: Option, + pub merchant_details: Option, + pub publishable_key: Option, + pub storage_scheme: Option, + pub metadata: Option, + pub routing_algorithm: Option, + pub modified_at: time::PrimitiveDateTime, + pub frm_routing_algorithm: Option, + pub payout_routing_algorithm: Option, + pub organization_id: Option, + pub recon_status: Option, +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = merchant_account)] pub struct MerchantAccountUpdateInternal { pub merchant_name: Option, pub merchant_details: Option, pub return_url: Option, - pub webhook_details: Option, + pub webhook_details: Option, pub sub_merchants_enabled: Option, pub parent_merchant_id: Option, pub enable_payment_response_hash: Option, @@ -342,12 +307,12 @@ pub struct MerchantAccountUpdateInternal { pub metadata: Option, pub routing_algorithm: Option, pub primary_business_details: Option, - pub modified_at: Option, + pub modified_at: time::PrimitiveDateTime, pub intent_fulfillment_time: Option, pub frm_routing_algorithm: Option, pub payout_routing_algorithm: Option, pub organization_id: Option, - pub is_recon_enabled: bool, + pub is_recon_enabled: Option, pub default_profile: Option>, pub recon_status: Option, pub payment_link_config: Option, diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 23f9dff31ea1..91c02b69034d 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -639,39 +639,20 @@ diesel::table! { use crate::enums::diesel_exports::*; merchant_account (id) { - #[max_length = 255] - return_url -> Nullable, - enable_payment_response_hash -> Bool, - #[max_length = 255] - payment_response_hash_key -> Nullable, - redirect_to_merchant_with_http_post -> Bool, merchant_name -> Nullable, merchant_details -> Nullable, - webhook_details -> Nullable, - sub_merchants_enabled -> Nullable, - #[max_length = 64] - parent_merchant_id -> Nullable, #[max_length = 128] publishable_key -> Nullable, storage_scheme -> MerchantStorageScheme, - #[max_length = 64] - locker_id -> Nullable, metadata -> Nullable, routing_algorithm -> Nullable, - primary_business_details -> Json, - intent_fulfillment_time -> Nullable, created_at -> Timestamp, modified_at -> Timestamp, frm_routing_algorithm -> Nullable, payout_routing_algorithm -> Nullable, #[max_length = 32] organization_id -> Varchar, - is_recon_enabled -> Bool, - #[max_length = 64] - default_profile -> Nullable, recon_status -> ReconStatus, - payment_link_config -> Nullable, - pm_collect_link_config -> Nullable, #[max_length = 64] id -> Varchar, } diff --git a/crates/hyperswitch_domain_models/src/merchant_account.rs b/crates/hyperswitch_domain_models/src/merchant_account.rs index d8af32255060..1697b78275c6 100644 --- a/crates/hyperswitch_domain_models/src/merchant_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_account.rs @@ -131,65 +131,52 @@ impl From for MerchantAccount { /// Set the private fields of merchant account pub struct MerchantAccountSetter { pub id: common_utils::id_type::MerchantId, - pub return_url: Option, - pub enable_payment_response_hash: bool, - pub payment_response_hash_key: Option, - pub redirect_to_merchant_with_http_post: bool, pub merchant_name: OptionalEncryptableName, pub merchant_details: OptionalEncryptableValue, - pub webhook_details: Option, - pub sub_merchants_enabled: Option, - pub parent_merchant_id: Option, pub publishable_key: String, pub storage_scheme: MerchantStorageScheme, - pub locker_id: Option, pub metadata: Option, pub routing_algorithm: Option, - pub primary_business_details: serde_json::Value, pub frm_routing_algorithm: Option, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, - pub intent_fulfillment_time: Option, pub payout_routing_algorithm: Option, pub organization_id: common_utils::id_type::OrganizationId, - pub is_recon_enabled: bool, - pub default_profile: Option, pub recon_status: diesel_models::enums::ReconStatus, - pub payment_link_config: Option, - pub pm_collect_link_config: Option, } #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] impl From for MerchantAccount { fn from(item: MerchantAccountSetter) -> Self { + let MerchantAccountSetter { + id, + merchant_name, + merchant_details, + publishable_key, + storage_scheme, + metadata, + routing_algorithm, + frm_routing_algorithm, + created_at, + modified_at, + payout_routing_algorithm, + organization_id, + recon_status, + } = item; Self { - id: item.id, - return_url: item.return_url, - enable_payment_response_hash: item.enable_payment_response_hash, - payment_response_hash_key: item.payment_response_hash_key, - redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, - merchant_name: item.merchant_name, - merchant_details: item.merchant_details, - webhook_details: item.webhook_details, - sub_merchants_enabled: item.sub_merchants_enabled, - parent_merchant_id: item.parent_merchant_id, - publishable_key: item.publishable_key, - storage_scheme: item.storage_scheme, - locker_id: item.locker_id, - metadata: item.metadata, - routing_algorithm: item.routing_algorithm, - primary_business_details: item.primary_business_details, - frm_routing_algorithm: item.frm_routing_algorithm, - created_at: item.created_at, - modified_at: item.modified_at, - intent_fulfillment_time: item.intent_fulfillment_time, - payout_routing_algorithm: item.payout_routing_algorithm, - organization_id: item.organization_id, - is_recon_enabled: item.is_recon_enabled, - default_profile: item.default_profile, - recon_status: item.recon_status, - payment_link_config: item.payment_link_config, - pm_collect_link_config: item.pm_collect_link_config, + id, + merchant_name, + merchant_details, + publishable_key, + storage_scheme, + metadata, + routing_algorithm, + frm_routing_algorithm, + created_at, + modified_at, + payout_routing_algorithm, + organization_id, + recon_status, } } } @@ -198,32 +185,18 @@ impl From for MerchantAccount { #[derive(Clone, Debug, serde::Serialize)] pub struct MerchantAccount { id: common_utils::id_type::MerchantId, - pub return_url: Option, - pub enable_payment_response_hash: bool, - pub payment_response_hash_key: Option, - pub redirect_to_merchant_with_http_post: bool, pub merchant_name: OptionalEncryptableName, pub merchant_details: OptionalEncryptableValue, - pub webhook_details: Option, - pub sub_merchants_enabled: Option, - pub parent_merchant_id: Option, pub publishable_key: String, pub storage_scheme: MerchantStorageScheme, - pub locker_id: Option, pub metadata: Option, pub routing_algorithm: Option, - pub primary_business_details: serde_json::Value, pub frm_routing_algorithm: Option, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, - pub intent_fulfillment_time: Option, pub payout_routing_algorithm: Option, pub organization_id: common_utils::id_type::OrganizationId, - pub is_recon_enabled: bool, - pub default_profile: Option, pub recon_status: diesel_models::enums::ReconStatus, - pub payment_link_config: Option, - pub pm_collect_link_config: Option, } impl MerchantAccount { @@ -243,6 +216,10 @@ impl MerchantAccount { } } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum MerchantAccountUpdate { @@ -278,6 +255,33 @@ pub enum MerchantAccountUpdate { ModifiedAtUpdate, } +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum MerchantAccountUpdate { + Update { + merchant_name: OptionalEncryptableName, + merchant_details: OptionalEncryptableValue, + publishable_key: Option, + metadata: Option, + routing_algorithm: Option, + frm_routing_algorithm: Option, + payout_routing_algorithm: Option, + }, + StorageSchemeUpdate { + storage_scheme: MerchantStorageScheme, + }, + ReconUpdate { + recon_status: diesel_models::enums::ReconStatus, + }, + ModifiedAtUpdate, +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] + impl From for MerchantAccountUpdateInternal { fn from(merchant_account_update: MerchantAccountUpdate) -> Self { let now = date_time::now(); @@ -286,8 +290,8 @@ impl From for MerchantAccountUpdateInternal { MerchantAccountUpdate::Update { merchant_name, merchant_details, - return_url, webhook_details, + return_url, routing_algorithm, sub_merchants_enabled, parent_merchant_id, @@ -308,11 +312,11 @@ impl From for MerchantAccountUpdateInternal { merchant_name: merchant_name.map(Encryption::from), merchant_details: merchant_details.map(Encryption::from), frm_routing_algorithm, - return_url, webhook_details, routing_algorithm, sub_merchants_enabled, parent_merchant_id, + return_url, enable_payment_response_hash, payment_response_hash_key, redirect_to_merchant_with_http_post, @@ -320,32 +324,194 @@ impl From for MerchantAccountUpdateInternal { locker_id, metadata, primary_business_details, - modified_at: Some(now), + modified_at: now, intent_fulfillment_time, payout_routing_algorithm, default_profile, payment_link_config, pm_collect_link_config, - ..Default::default() + storage_scheme: None, + organization_id: None, + is_recon_enabled: None, + recon_status: None, }, MerchantAccountUpdate::StorageSchemeUpdate { storage_scheme } => Self { storage_scheme: Some(storage_scheme), - modified_at: Some(now), - ..Default::default() + modified_at: now, + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + locker_id: None, + metadata: None, + routing_algorithm: None, + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, + is_recon_enabled: None, + default_profile: None, + recon_status: None, + payment_link_config: None, + pm_collect_link_config: None, }, MerchantAccountUpdate::ReconUpdate { recon_status } => Self { recon_status: Some(recon_status), - modified_at: Some(now), - ..Default::default() + modified_at: now, + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + storage_scheme: None, + locker_id: None, + metadata: None, + routing_algorithm: None, + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, + is_recon_enabled: None, + default_profile: None, + payment_link_config: None, + pm_collect_link_config: None, }, MerchantAccountUpdate::UnsetDefaultProfile => Self { default_profile: Some(None), - modified_at: Some(now), - ..Default::default() + modified_at: now, + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + storage_scheme: None, + locker_id: None, + metadata: None, + routing_algorithm: None, + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, + is_recon_enabled: None, + recon_status: None, + payment_link_config: None, + pm_collect_link_config: None, + }, + MerchantAccountUpdate::ModifiedAtUpdate => Self { + modified_at: now, + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + storage_scheme: None, + locker_id: None, + metadata: None, + routing_algorithm: None, + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, + is_recon_enabled: None, + default_profile: None, + recon_status: None, + payment_link_config: None, + pm_collect_link_config: None, + }, + } + } +} + +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] +impl From for MerchantAccountUpdateInternal { + fn from(merchant_account_update: MerchantAccountUpdate) -> Self { + let now = date_time::now(); + + match merchant_account_update { + MerchantAccountUpdate::Update { + merchant_name, + merchant_details, + routing_algorithm, + publishable_key, + metadata, + frm_routing_algorithm, + payout_routing_algorithm, + } => Self { + merchant_name: merchant_name.map(Encryption::from), + merchant_details: merchant_details.map(Encryption::from), + frm_routing_algorithm, + routing_algorithm, + publishable_key, + metadata, + modified_at: now, + payout_routing_algorithm, + storage_scheme: None, + organization_id: None, + recon_status: None, + }, + MerchantAccountUpdate::StorageSchemeUpdate { storage_scheme } => Self { + storage_scheme: Some(storage_scheme), + modified_at: now, + merchant_name: None, + merchant_details: None, + publishable_key: None, + metadata: None, + routing_algorithm: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, + recon_status: None, + }, + MerchantAccountUpdate::ReconUpdate { recon_status } => Self { + recon_status: Some(recon_status), + modified_at: now, + merchant_name: None, + merchant_details: None, + publishable_key: None, + storage_scheme: None, + metadata: None, + routing_algorithm: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, }, MerchantAccountUpdate::ModifiedAtUpdate => Self { - modified_at: Some(date_time::now()), - ..Default::default() + modified_at: now, + merchant_name: None, + merchant_details: None, + publishable_key: None, + storage_scheme: None, + metadata: None, + routing_algorithm: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, + recon_status: None, }, } } @@ -361,32 +527,18 @@ impl super::behaviour::Conversion for MerchantAccount { let setter = diesel_models::merchant_account::MerchantAccountSetter { id, - return_url: self.return_url, - enable_payment_response_hash: self.enable_payment_response_hash, - payment_response_hash_key: self.payment_response_hash_key, - redirect_to_merchant_with_http_post: self.redirect_to_merchant_with_http_post, merchant_name: self.merchant_name.map(|name| name.into()), merchant_details: self.merchant_details.map(|details| details.into()), - webhook_details: self.webhook_details, - sub_merchants_enabled: self.sub_merchants_enabled, - parent_merchant_id: self.parent_merchant_id, publishable_key: Some(self.publishable_key), storage_scheme: self.storage_scheme, - locker_id: self.locker_id, metadata: self.metadata, routing_algorithm: self.routing_algorithm, - primary_business_details: self.primary_business_details, created_at: self.created_at, modified_at: self.modified_at, - intent_fulfillment_time: self.intent_fulfillment_time, frm_routing_algorithm: self.frm_routing_algorithm, payout_routing_algorithm: self.payout_routing_algorithm, organization_id: self.organization_id, - is_recon_enabled: self.is_recon_enabled, - default_profile: self.default_profile, recon_status: self.recon_status, - payment_link_config: self.payment_link_config, - pm_collect_link_config: self.pm_collect_link_config, }; Ok(diesel_models::MerchantAccount::from(setter)) @@ -411,10 +563,6 @@ impl super::behaviour::Conversion for MerchantAccount { async { Ok::>(Self { id, - return_url: item.return_url, - enable_payment_response_hash: item.enable_payment_response_hash, - payment_response_hash_key: item.payment_response_hash_key, - redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, merchant_name: item .merchant_name .async_lift(|inner| { @@ -427,26 +575,16 @@ impl super::behaviour::Conversion for MerchantAccount { decrypt_optional(state, inner, key_manager_identifier.clone(), key.peek()) }) .await?, - webhook_details: item.webhook_details, - sub_merchants_enabled: item.sub_merchants_enabled, - parent_merchant_id: item.parent_merchant_id, publishable_key, storage_scheme: item.storage_scheme, - locker_id: item.locker_id, metadata: item.metadata, routing_algorithm: item.routing_algorithm, frm_routing_algorithm: item.frm_routing_algorithm, - primary_business_details: item.primary_business_details, created_at: item.created_at, modified_at: item.modified_at, - intent_fulfillment_time: item.intent_fulfillment_time, payout_routing_algorithm: item.payout_routing_algorithm, organization_id: item.organization_id, - is_recon_enabled: item.is_recon_enabled, - default_profile: item.default_profile, recon_status: item.recon_status, - payment_link_config: item.payment_link_config, - pm_collect_link_config: item.pm_collect_link_config, }) } .await @@ -461,29 +599,15 @@ impl super::behaviour::Conversion for MerchantAccount { id: self.id, merchant_name: self.merchant_name.map(Encryption::from), merchant_details: self.merchant_details.map(Encryption::from), - return_url: self.return_url, - webhook_details: self.webhook_details, - sub_merchants_enabled: self.sub_merchants_enabled, - parent_merchant_id: self.parent_merchant_id, - enable_payment_response_hash: Some(self.enable_payment_response_hash), - payment_response_hash_key: self.payment_response_hash_key, - redirect_to_merchant_with_http_post: Some(self.redirect_to_merchant_with_http_post), publishable_key: Some(self.publishable_key), - locker_id: self.locker_id, metadata: self.metadata, routing_algorithm: self.routing_algorithm, - primary_business_details: self.primary_business_details, created_at: now, modified_at: now, - intent_fulfillment_time: self.intent_fulfillment_time, frm_routing_algorithm: self.frm_routing_algorithm, payout_routing_algorithm: self.payout_routing_algorithm, organization_id: self.organization_id, - is_recon_enabled: self.is_recon_enabled, - default_profile: self.default_profile, recon_status: self.recon_status, - payment_link_config: self.payment_link_config, - pm_collect_link_config: self.pm_collect_link_config, }) } } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 958b2d286d18..78e5c74fc06a 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -37,6 +37,7 @@ v1 = ["api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "stor customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2"] merchant_account_v2 = ["api_models/merchant_account_v2", "diesel_models/merchant_account_v2", "hyperswitch_domain_models/merchant_account_v2"] payment_v2 = ["api_models/payment_v2", "diesel_models/payment_v2", "hyperswitch_domain_models/payment_v2"] +payment_methods_v2 = ["api_models/payment_methods_v2"] routing_v2 = ["api_models/routing_v2"] merchant_connector_account_v2 = ["api_models/merchant_connector_account_v2", "kgraph_utils/merchant_connector_account_v2", "hyperswitch_domain_models/merchant_connector_account_v2", "diesel_models/merchant_connector_account_v2"] diff --git a/crates/router/src/compatibility/stripe/customers/types.rs b/crates/router/src/compatibility/stripe/customers/types.rs index e2acc7b17b5b..1dc431753429 100644 --- a/crates/router/src/compatibility/stripe/customers/types.rs +++ b/crates/router/src/compatibility/stripe/customers/types.rs @@ -193,7 +193,7 @@ pub struct CustomerPaymentMethodListResponse { #[derive(Default, Serialize, PartialEq, Eq)] pub struct PaymentMethodData { - pub id: String, + pub id: Option, pub object: &'static str, pub card: Option, pub created: Option, @@ -222,12 +222,29 @@ impl From for CustomerPaymentMethodList } } +// Check this in review impl From for PaymentMethodData { fn from(item: api_types::CustomerPaymentMethod) -> Self { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + let card = item.card.map(From::from); + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + let card = match item.payment_method_data { + Some(api_types::PaymentMethodListData::Card(card)) => Some(CardDetails::from(card)), + _ => None, + }; Self { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + id: Some(item.payment_token), + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] id: item.payment_token, object: "payment_method", - card: item.card.map(From::from), + card, created: item.created, } } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index be29da50aed9..c34c00599a55 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -167,6 +167,7 @@ impl Default for Mandates { enums::Connector::Bankofamerica, enums::Connector::Noon, enums::Connector::Cybersource, + enums::Connector::Wellsfargo, ]), }, ), @@ -178,6 +179,7 @@ impl Default for Mandates { enums::Connector::Adyen, enums::Connector::Bankofamerica, enums::Connector::Cybersource, + enums::Connector::Wellsfargo, ]), }, ), @@ -202,6 +204,7 @@ impl Default for Mandates { enums::Connector::Stripe, enums::Connector::Bankofamerica, enums::Connector::Cybersource, + enums::Connector::Wellsfargo, ]), }, ), @@ -2769,6 +2772,129 @@ impl Default for super::settings::RequiredFields { common: HashMap::new() } ), + ( + enums::Connector::Wellsfargo, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common:HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ) + ] + ), + } + ), ( enums::Connector::Worldline, RequiredFieldFinal { @@ -5444,6 +5570,129 @@ impl Default for super::settings::RequiredFields { common: HashMap::new() } ), + ( + enums::Connector::Wellsfargo, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate:HashMap::new(), + common: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ) + ] + ), + } + ), ( enums::Connector::Worldline, RequiredFieldFinal { @@ -7320,20 +7569,174 @@ impl Default for super::settings::RequiredFields { ), common: HashMap::new(), } - ) - ]), - }, - ), - ( - enums::PaymentMethodType::GooglePay, - ConnectorFields { - fields: HashMap::from([ + ), ( - enums::Connector::Adyen, + enums::Connector::Wellsfargo, RequiredFieldFinal { mandate: HashMap::new(), - non_mandate: HashMap::new(), - common: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "shipping.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.first_name".to_string(), + display_name: "shipping_first_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.last_name".to_string(), + display_name: "shipping_last_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.city".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserShippingAddressCity, + value: None, + } + ), + ( + "shipping.address.state".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserShippingAddressState, + value: None, + } + ), + ( + "shipping.address.zip".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserShippingAddressPincode, + value: None, + } + ), + ( + "shipping.address.country".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserShippingAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "shipping.address.line1".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserShippingAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ) + ]), + }, + ), + ( + enums::PaymentMethodType::GooglePay, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), } ), ( @@ -7886,6 +8289,160 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Wellsfargo, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "shipping.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.first_name".to_string(), + display_name: "shipping_first_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.last_name".to_string(), + display_name: "shipping_last_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.city".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserShippingAddressCity, + value: None, + } + ), + ( + "shipping.address.state".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserShippingAddressState, + value: None, + } + ), + ( + "shipping.address.zip".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserShippingAddressPincode, + value: None, + } + ), + ( + "shipping.address.country".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserShippingAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "shipping.address.line1".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserShippingAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ]), }, ), diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index fe15422a1284..85b310397b26 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -1,10 +1,14 @@ -pub mod braintree_graphql_transformers; pub mod transformers; -use std::{fmt::Debug, str::FromStr}; +use std::str::FromStr; use api_models::webhooks::IncomingWebhookEvent; use base64::Engine; -use common_utils::{crypto, ext_traits::XmlExt, request::RequestContent}; +use common_utils::{ + crypto, + ext_traits::XmlExt, + request::RequestContent, + types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector}, +}; use diesel_models::enums; use error_stack::{report, Report, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; @@ -37,19 +41,22 @@ use crate::{ utils::{self, BytesExt}, }; -#[derive(Debug, Clone)] -pub struct Braintree; - -pub const BRAINTREE_VERSION: &str = "Braintree-Version"; -pub const BRAINTREE_VERSION_VALUE: &str = "2019-01-01"; -pub const BRAINTREE_API_VERSION: &str = "graphql_api"; +#[derive(Clone)] +pub struct Braintree { + amount_converter: &'static (dyn AmountConvertor + Sync), +} impl Braintree { - pub fn is_braintree_graphql_version(&self, connector_api_version: &Option) -> bool { - *connector_api_version == Some(BRAINTREE_API_VERSION.to_string()) + pub const fn new() -> &'static Self { + &Self { + amount_converter: &StringMajorUnitForConnector, + } } } +pub const BRAINTREE_VERSION: &str = "Braintree-Version"; +pub const BRAINTREE_VERSION_VALUE: &str = "2019-01-01"; + impl ConnectorCommonExt for Braintree where Self: ConnectorIntegration, @@ -94,9 +101,11 @@ impl ConnectorCommon for Braintree { ) -> CustomResult)>, errors::ConnectorError> { let auth = braintree::BraintreeAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth_key = format!("{}:{}", auth.public_key.peek(), auth.private_key.peek()); + let auth_header = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_key)); Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.auth_header.into_masked(), + auth_header.into_masked(), )]) } @@ -105,11 +114,13 @@ impl ConnectorCommon for Braintree { res: types::Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: Result> = - res.response.parse_struct("Braintree Error Response"); + let response: Result< + braintree::ErrorResponses, + Report, + > = res.response.parse_struct("Braintree Error Response"); match response { - Ok(braintree::ErrorResponse::BraintreeApiErrorResponse(response)) => { + Ok(braintree::ErrorResponses::BraintreeApiErrorResponse(response)) => { event_builder.map(|i| i.set_error_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -139,7 +150,7 @@ impl ConnectorCommon for Braintree { connector_transaction_id: None, }) } - Ok(braintree::ErrorResponse::BraintreeErrorResponse(response)) => { + Ok(braintree::ErrorResponses::BraintreeErrorResponse(response)) => { event_builder.map(|i| i.set_error_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -205,109 +216,6 @@ impl ConnectorIntegration for Braintree { - fn get_headers( - &self, - req: &types::PaymentsSessionRouterData, - _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - let mut headers = vec![ - ( - headers::CONTENT_TYPE.to_string(), - types::PaymentsSessionType::get_content_type(self) - .to_string() - .into(), - ), - (headers::X_API_VERSION.to_string(), "6".to_string().into()), - ( - headers::ACCEPT.to_string(), - "application/json".to_string().into(), - ), - ]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - headers.append(&mut api_key); - Ok(headers) - } - - fn get_content_type(&self) -> &'static str { - "application/json" - } - - fn get_url( - &self, - req: &types::PaymentsSessionRouterData, - connectors: &settings::Connectors, - ) -> CustomResult { - let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(format!( - "{}/merchants/{}/client_token", - self.base_url(connectors), - auth_type.merchant_id.peek(), - )) - } - - fn build_request( - &self, - req: &types::PaymentsSessionRouterData, - connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => Ok(None), - false => { - let request = Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::PaymentsSessionType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsSessionType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsSessionType::get_request_body( - self, req, connectors, - )?) - .build(), - ); - Ok(request) - } - } - } - - fn get_error_response( - &self, - res: types::Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) - } - - fn get_request_body( - &self, - req: &types::PaymentsSessionRouterData, - _connectors: &settings::Connectors, - ) -> CustomResult { - let connector_req = braintree::BraintreeSessionRequest::try_from(req)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - - fn handle_response( - &self, - data: &types::PaymentsSessionRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: types::Response, - ) -> CustomResult { - let response: braintree::BraintreeSessionTokenResponse = res - .response - .parse_struct("braintree SessionTokenResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } } impl api::PaymentToken for Braintree {} @@ -336,12 +244,7 @@ impl _req: &types::TokenizationRouterData, connectors: &settings::Connectors, ) -> CustomResult { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) + Ok(self.base_url(connectors).to_string()) } fn get_request_body( @@ -349,7 +252,7 @@ impl req: &types::TokenizationRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_req = braintree_graphql_transformers::BraintreeTokenRequest::try_from(req)?; + let connector_req = transformers::BraintreeTokenRequest::try_from(req)?; Ok(RequestContent::Json(Box::new(connector_req))) } @@ -359,23 +262,18 @@ impl req: &types::TokenizationRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::TokenizationType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .set_body(types::TokenizationType::get_request_body( - self, req, connectors, - )?) - .build(), - )), - false => Ok(None), - } + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::TokenizationType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::TokenizationType::get_headers(self, req, connectors)?) + .set_body(types::TokenizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } - fn handle_response( &self, data: &types::TokenizationRouterData, @@ -385,7 +283,7 @@ impl where types::PaymentsResponseData: Clone, { - let response: braintree_graphql_transformers::BraintreeTokenResponse = res + let response: transformers::BraintreeTokenResponse = res .response .parse_struct("BraintreeTokenResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -442,11 +340,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version.clone(); - match self.is_braintree_graphql_version(connector_api_version) { - true => self.build_headers(req, connectors), - false => Ok(vec![]), - } + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -455,23 +349,10 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = req.connector_api_version.clone(); - match self.is_braintree_graphql_version(&connector_api_version) { - true => { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) - } - false => { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - } + Ok(self.base_url(connectors).to_string()) } fn get_request_body( @@ -479,28 +360,16 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version.clone(); - let connector_router_data = - braintree_graphql_transformers::BraintreeRouterData::try_from(( - &self.get_currency_unit(), - req.request.currency, - req.request.amount_to_capture, - req, - ))?; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let connector_req = - braintree_graphql_transformers::BraintreeCaptureRequest::try_from( - &connector_router_data, - )?; + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + let connector_router_data = braintree::BraintreeRouterData::try_from((amount, req))?; + let connector_req = + transformers::BraintreeCaptureRequest::try_from(&connector_router_data)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - false => Err(errors::ConnectorError::NotImplemented( - "get_request_body method".to_string(), - ) - .into()), - } + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -508,26 +377,19 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( - self, req, connectors, - )?) - .build(), - )), - false => Err(errors::ConnectorError::NotImplemented( - "Capture flow not Implemented".to_string(), - ) - .into()), - } + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } fn handle_response( @@ -536,7 +398,7 @@ impl ConnectorIntegration, res: types::Response, ) -> CustomResult { - let response: braintree_graphql_transformers::BraintreeCaptureResponse = res + let response: transformers::BraintreeCaptureResponse = res .response .parse_struct("Braintree PaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -566,28 +428,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => self.build_headers(req, connectors), - false => { - let mut headers = vec![ - ( - headers::CONTENT_TYPE.to_string(), - types::PaymentsSyncType::get_content_type(self) - .to_string() - .into(), - ), - (headers::X_API_VERSION.to_string(), "6".to_string().into()), - ( - headers::ACCEPT.to_string(), - "application/json".to_string().into(), - ), - ]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - headers.append(&mut api_key); - Ok(headers) - } - } + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -596,34 +437,10 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) - } - false => { - let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - let connector_payment_id = req - .request - .connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; - Ok(format!( - "{}/merchants/{}/transactions/{connector_payment_id}", - self.base_url(connectors), - auth_type.merchant_id.peek() - )) - } - } + Ok(self.base_url(connectors).to_string()) } fn get_request_body( @@ -631,16 +448,9 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let connector_req = - braintree_graphql_transformers::BraintreePSyncRequest::try_from(req)?; + let connector_req = transformers::BraintreePSyncRequest::try_from(req)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - false => Err(report!(errors::ConnectorError::RequestEncodingFailed)), - } + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -648,31 +458,17 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .set_body(types::PaymentsSyncType::get_request_body( - self, req, connectors, - )?) - .build(), - )), - false => Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Get) - .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .set_body(types::PaymentsSyncType::get_request_body( - self, req, connectors, - )?) - .build(), - )), - } + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } fn handle_response( @@ -681,35 +477,17 @@ impl ConnectorIntegration, res: types::Response, ) -> CustomResult { - let connector_api_version = &data.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let response: braintree_graphql_transformers::BraintreePSyncResponse = res - .response - .parse_struct("Braintree PaymentSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - false => { - let response: braintree::BraintreePaymentsResponse = res - .response - .parse_struct("Braintree PaymentsResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - } + let response: transformers::BraintreePSyncResponse = res + .response + .parse_struct("Braintree PaymentSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } fn get_error_response( @@ -729,56 +507,15 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => self.build_headers(req, connectors), - false => { - let mut headers = vec![ - ( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self) - .to_string() - .into(), - ), - (headers::X_API_VERSION.to_string(), "6".to_string().into()), - ( - headers::ACCEPT.to_string(), - "application/json".to_string().into(), - ), - ]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - headers.append(&mut api_key); - Ok(headers) - } - } + self.build_headers(req, connectors) } fn get_url( &self, - req: &types::PaymentsAuthorizeRouterData, + _req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) - } - false => { - let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - Ok(format!( - "{}merchants/{}/transactions", - self.base_url(connectors), - auth_type.merchant_id.peek() - )) - } - } + Ok(self.base_url(connectors).to_string()) } fn build_request( @@ -808,27 +545,15 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version; - let connector_router_data = - braintree_graphql_transformers::BraintreeRouterData::try_from(( - &self.get_currency_unit(), - req.request.currency, - req.request.amount, - req, - ))?; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let connector_req = - braintree_graphql_transformers::BraintreePaymentsRequest::try_from( - &connector_router_data, - )?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - false => { - let connector_req = braintree::BraintreePaymentsRequest::try_from(req)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - } + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.currency, + )?; + let connector_router_data = braintree::BraintreeRouterData::try_from((amount, req))?; + let connector_req: transformers::BraintreePaymentsRequest = + transformers::BraintreePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -837,38 +562,9 @@ impl ConnectorIntegration, res: types::Response, ) -> CustomResult { - let connector_api_version = &data.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => match data.request.is_auto_capture()? { - true => { - let response: braintree_graphql_transformers::BraintreePaymentsResponse = res - .response - .parse_struct("Braintree PaymentsResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - false => { - let response: braintree_graphql_transformers::BraintreeAuthResponse = res - .response - .parse_struct("Braintree AuthResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - }, - false => { - let response: braintree::BraintreePaymentsResponse = res + match data.request.is_auto_capture()? { + true => { + let response: transformers::BraintreePaymentsResponse = res .response .parse_struct("Braintree PaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -880,6 +576,19 @@ impl ConnectorIntegration { + let response: transformers::BraintreeAuthResponse = res + .response + .parse_struct("Braintree AuthResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } } } @@ -900,28 +609,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => self.build_headers(req, connectors), - false => { - let mut headers = vec![ - ( - headers::CONTENT_TYPE.to_string(), - types::PaymentsVoidType::get_content_type(self) - .to_string() - .into(), - ), - (headers::X_API_VERSION.to_string(), "6".to_string().into()), - ( - headers::ACCEPT.to_string(), - "application/json".to_string().into(), - ), - ]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - headers.append(&mut api_key); - Ok(headers) - } - } + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -930,30 +618,10 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) - } - false => { - let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(format!( - "{}merchants/{}/transactions/{}/void", - self.base_url(connectors), - auth_type.merchant_id.peek(), - req.request.connector_transaction_id - )) - } - } + Ok(self.base_url(connectors).to_string()) } fn build_request( @@ -979,15 +647,8 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let connector_req = - braintree_graphql_transformers::BraintreeCancelRequest::try_from(req)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - false => Err(report!(errors::ConnectorError::RequestEncodingFailed)), - } + let connector_req = transformers::BraintreeCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -996,37 +657,18 @@ impl ConnectorIntegration, res: types::Response, ) -> CustomResult { - let connector_api_version = &data.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let response: braintree_graphql_transformers::BraintreeCancelResponse = res - .response - .parse_struct("Braintree VoidResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - false => { - let response: braintree::BraintreePaymentsResponse = res - .response - .parse_struct("Braintree PaymentsVoidResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - } + let response: transformers::BraintreeCancelResponse = res + .response + .parse_struct("Braintree VoidResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } - fn get_error_response( &self, res: types::Response, @@ -1048,28 +690,7 @@ impl ConnectorIntegration, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => self.build_headers(req, connectors), - false => { - let mut headers = vec![ - ( - headers::CONTENT_TYPE.to_string(), - types::RefundExecuteType::get_content_type(self) - .to_string() - .into(), - ), - (headers::X_API_VERSION.to_string(), "6".to_string().into()), - ( - headers::ACCEPT.to_string(), - "application/json".to_string().into(), - ), - ]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - headers.append(&mut api_key); - Ok(headers) - } - } + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -1078,31 +699,10 @@ impl ConnectorIntegration, + _req: &types::RefundsRouterData, connectors: &settings::Connectors, ) -> CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) - } - false => { - let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - let connector_payment_id = req.request.connector_transaction_id.clone(); - Ok(format!( - "{}merchants/{}/transactions/{}", - self.base_url(connectors), - auth_type.merchant_id.peek(), - connector_payment_id - )) - } - } + Ok(self.base_url(connectors).to_string()) } fn get_request_body( @@ -1110,29 +710,15 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_api_version = &req.connector_api_version; - let connector_router_data = - braintree_graphql_transformers::BraintreeRouterData::try_from(( - &self.get_currency_unit(), - req.request.currency, - req.request.refund_amount, - req, - ))?; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let connector_req = - braintree_graphql_transformers::BraintreeRefundRequest::try_from( - connector_router_data, - )?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - false => { - let connector_req = braintree::BraintreeRefundRequest::try_from(req)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - } + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_refund_amount, + req.request.currency, + )?; + let connector_router_data = braintree::BraintreeRouterData::try_from((amount, req))?; + let connector_req = transformers::BraintreeRefundRequest::try_from(connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } - fn build_request( &self, req: &types::RefundsRouterData, @@ -1158,35 +744,17 @@ impl ConnectorIntegration, res: types::Response, ) -> CustomResult, errors::ConnectorError> { - let connector_api_version = &data.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let response: braintree_graphql_transformers::BraintreeRefundResponse = res - .response - .parse_struct("Braintree RefundResponse") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - false => { - let response: braintree::RefundResponse = res - .response - .parse_struct("Braintree RefundResponse") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - } + let response: transformers::BraintreeRefundResponse = res + .response + .parse_struct("Braintree RefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } fn get_error_response( @@ -1206,11 +774,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => self.build_headers(req, connectors), - false => Ok(vec![]), - } + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { @@ -1219,23 +783,10 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) - } - false => { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - } + Ok(self.base_url(connectors).to_string()) } fn get_request_body( @@ -1243,15 +794,8 @@ impl ConnectorIntegration CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let connector_req = - braintree_graphql_transformers::BraintreeRSyncRequest::try_from(req)?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - false => Err(report!(errors::ConnectorError::RequestEncodingFailed)), - } + let connector_req = transformers::BraintreeRSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1259,21 +803,17 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::RefundSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .set_body(types::RefundSyncType::get_request_body( - self, req, connectors, - )?) - .build(), - )), - false => Ok(None), - } + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .set_body(types::RefundSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } fn handle_response( @@ -1285,35 +825,17 @@ impl ConnectorIntegration, errors::ConnectorError, > { - let connector_api_version = &data.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let response: braintree_graphql_transformers::BraintreeRSyncResponse = res - .response - .parse_struct("Braintree RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - false => { - let response: braintree::RefundResponse = res - .response - .parse_struct("Braintree RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - } + let response: transformers::BraintreeRSyncResponse = res + .response + .parse_struct("Braintree RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } fn get_error_response( &self, @@ -1485,9 +1007,7 @@ impl api::IncomingWebhook for Braintree { currency, )?, currency: dispute_data.currency_iso_code, - dispute_stage: braintree_graphql_transformers::get_dispute_stage( - dispute_data.kind.as_str(), - )?, + dispute_stage: transformers::get_dispute_stage(dispute_data.kind.as_str())?, connector_dispute_id: dispute_data.id, connector_reason: dispute_data.reason, connector_reason_code: dispute_data.reason_code, @@ -1516,16 +1036,15 @@ fn get_matching_webhook_signature( fn get_webhook_object_from_body( body: &[u8], -) -> CustomResult { - serde_urlencoded::from_bytes::(body) - .change_context(errors::ParsingError::StructParseFailure( - "BraintreeWebhookResponse", - )) +) -> CustomResult { + serde_urlencoded::from_bytes::(body).change_context( + errors::ParsingError::StructParseFailure("BraintreeWebhookResponse"), + ) } fn decode_webhook_payload( payload: &[u8], -) -> CustomResult { +) -> CustomResult { let decoded_response = consts::BASE64_ENGINE .decode(payload) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; @@ -1534,7 +1053,7 @@ fn decode_webhook_payload( .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; xml_response - .parse_xml::() + .parse_xml::() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) } @@ -1548,17 +1067,16 @@ impl services::ConnectorRedirectResponse for Braintree { match action { services::PaymentAction::PSync => match json_payload { Some(payload) => { - let redirection_response:braintree_graphql_transformers::BraintreeRedirectionResponse = serde_json::from_value(payload) - - .change_context( - errors::ConnectorError::MissingConnectorRedirectionPayload { - field_name: "redirection_response", - }, - )?; + let redirection_response: transformers::BraintreeRedirectionResponse = + serde_json::from_value(payload).change_context( + errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "redirection_response", + }, + )?; let braintree_payload = - serde_json::from_str::< - braintree_graphql_transformers::BraintreeThreeDsErrorResponse, - >(&redirection_response.authentication_response); + serde_json::from_str::( + &redirection_response.authentication_response, + ); let (error_code, error_message) = match braintree_payload { Ok(braintree_response_payload) => ( braintree_response_payload.code, @@ -1597,62 +1115,33 @@ impl req: &types::PaymentsCompleteAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => self.build_headers(req, connectors), - false => Err(errors::ConnectorError::NotImplemented( - "get_headers method".to_string(), - ))?, - } + self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { self.common_get_content_type() } fn get_url( &self, - req: &types::PaymentsCompleteAuthorizeRouterData, + _req: &types::PaymentsCompleteAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let base_url = connectors - .braintree - .secondary_base_url - .as_ref() - .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?; - Ok(base_url.to_string()) - } - false => Err(errors::ConnectorError::NotImplemented( - "get_url method".to_string(), - ))?, - } + Ok(self.base_url(connectors).to_string()) } fn get_request_body( &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_router_data = - braintree_graphql_transformers::BraintreeRouterData::try_from(( - &self.get_currency_unit(), - req.request.currency, - req.request.amount, - req, - ))?; - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => { - let connector_req = - braintree_graphql_transformers::BraintreePaymentsRequest::try_from( - &connector_router_data, - )?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - false => Err(errors::ConnectorError::NotImplemented( - "get_request_body method".to_string(), - ))?, - } + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.currency, + )?; + let connector_router_data = braintree::BraintreeRouterData::try_from((amount, req))?; + + let connector_req = + transformers::BraintreePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1660,27 +1149,21 @@ impl req: &types::PaymentsCompleteAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let connector_api_version = &req.connector_api_version; - match self.is_braintree_graphql_version(connector_api_version) { - true => Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::PaymentsCompleteAuthorizeType::get_url( - self, req, connectors, - )?) - .attach_default_headers() - .headers(types::PaymentsCompleteAuthorizeType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, connectors, - )?) - .build(), - )), - false => Err(errors::ConnectorError::NotImplemented( - "payment method".to_string(), - ))?, - } + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } fn handle_response( &self, @@ -1691,7 +1174,7 @@ impl match connector_utils::PaymentsCompleteAuthorizeRequestData::is_auto_capture(&data.request)? { true => { - let response: braintree_graphql_transformers::BraintreeCompleteChargeResponse = res + let response: transformers::BraintreeCompleteChargeResponse = res .response .parse_struct("Braintree PaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -1704,7 +1187,7 @@ impl }) } false => { - let response: braintree_graphql_transformers::BraintreeCompleteAuthResponse = res + let response: transformers::BraintreeCompleteAuthResponse = res .response .parse_struct("Braintree AuthResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs deleted file mode 100644 index af50bd10ce77..000000000000 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ /dev/null @@ -1,1667 +0,0 @@ -use common_utils::pii; -use error_stack::ResultExt; -use masking::{ExposeInterface, Secret}; -use serde::{Deserialize, Serialize}; -use time::PrimitiveDateTime; - -use crate::{ - connector::utils::{ - self, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData, - RefundsRequestData, RouterData, - }, - consts, - core::errors, - services, - types::{self, api, domain, storage::enums, MandateReference}, - unimplemented_payment_method, -}; - -pub const CLIENT_TOKEN_MUTATION: &str = "mutation createClientToken($input: CreateClientTokenInput!) { createClientToken(input: $input) { clientToken}}"; -pub const TOKENIZE_CREDIT_CARD: &str = "mutation tokenizeCreditCard($input: TokenizeCreditCardInput!) { tokenizeCreditCard(input: $input) { clientMutationId paymentMethod { id } } }"; -pub const CHARGE_CREDIT_CARD_MUTATION: &str = "mutation ChargeCreditCard($input: ChargeCreditCardInput!) { chargeCreditCard(input: $input) { transaction { id legacyId createdAt amount { value currencyCode } status } } }"; -pub const AUTHORIZE_CREDIT_CARD_MUTATION: &str = "mutation authorizeCreditCard($input: AuthorizeCreditCardInput!) { authorizeCreditCard(input: $input) { transaction { id legacyId amount { value currencyCode } status } } }"; -pub const CAPTURE_TRANSACTION_MUTATION: &str = "mutation captureTransaction($input: CaptureTransactionInput!) { captureTransaction(input: $input) { clientMutationId transaction { id legacyId amount { value currencyCode } status } } }"; -pub const VOID_TRANSACTION_MUTATION: &str = "mutation voidTransaction($input: ReverseTransactionInput!) { reverseTransaction(input: $input) { clientMutationId reversal { ... on Transaction { id legacyId amount { value currencyCode } status } } } }"; -pub const REFUND_TRANSACTION_MUTATION: &str = "mutation refundTransaction($input: RefundTransactionInput!) { refundTransaction(input: $input) {clientMutationId refund { id legacyId amount { value currencyCode } status } } }"; -pub const AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION: &str="mutation authorizeCreditCard($input: AuthorizeCreditCardInput!) { authorizeCreditCard(input: $input) { transaction { id status createdAt paymentMethod { id } } } }"; -pub const CHARGE_AND_VAULT_TRANSACTION_MUTATION: &str ="mutation ChargeCreditCard($input: ChargeCreditCardInput!) { chargeCreditCard(input: $input) { transaction { id status createdAt paymentMethod { id } } } }"; - -#[derive(Debug, Serialize)] -pub struct BraintreeRouterData { - pub amount: String, - pub router_data: T, -} - -impl TryFrom<(&api::CurrencyUnit, enums::Currency, i64, T)> for BraintreeRouterData { - type Error = error_stack::Report; - fn try_from( - (currency_unit, currency, amount, item): (&api::CurrencyUnit, enums::Currency, i64, T), - ) -> Result { - let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; - Ok(Self { - amount, - router_data: item, - }) - } -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PaymentInput { - payment_method_id: Secret, - transaction: TransactionBody, -} - -#[derive(Debug, Serialize)] -pub struct VariablePaymentInput { - input: PaymentInput, -} - -#[derive(Debug, Serialize)] -pub struct CardPaymentRequest { - query: String, - variables: VariablePaymentInput, -} - -#[derive(Debug, Serialize)] -pub struct MandatePaymentRequest { - query: String, - variables: VariablePaymentInput, -} - -#[derive(Debug, Serialize)] -#[serde(untagged)] -pub enum BraintreePaymentsRequest { - Card(CardPaymentRequest), - CardThreeDs(BraintreeClientTokenRequest), - Mandate(MandatePaymentRequest), -} - -#[derive(Debug, Deserialize)] -pub struct BraintreeMeta { - merchant_account_id: Secret, - merchant_config_currency: enums::Currency, -} - -impl TryFrom<&Option> for BraintreeMeta { - type Error = error_stack::Report; - fn try_from(meta_data: &Option) -> Result { - let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) - .change_context(errors::ConnectorError::InvalidConnectorConfig { - config: "metadata", - })?; - Ok(metadata) - } -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RegularTransactionBody { - amount: String, - merchant_account_id: Secret, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VaultTransactionBody { - amount: String, - merchant_account_id: Secret, - vault_payment_method_after_transacting: TransactionTiming, -} - -#[derive(Debug, Serialize)] -#[serde(untagged)] -pub enum TransactionBody { - Regular(RegularTransactionBody), - Vault(VaultTransactionBody), -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TransactionTiming { - when: String, -} - -impl - TryFrom<( - &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, - String, - BraintreeMeta, - )> for MandatePaymentRequest -{ - type Error = error_stack::Report; - fn try_from( - (item, connector_mandate_id, metadata): ( - &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, - String, - BraintreeMeta, - ), - ) -> Result { - let (query, transaction_body) = ( - match item.router_data.request.is_auto_capture()? { - true => CHARGE_CREDIT_CARD_MUTATION.to_string(), - false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(), - }, - TransactionBody::Regular(RegularTransactionBody { - amount: item.amount.to_owned(), - merchant_account_id: metadata.merchant_account_id, - }), - ); - Ok(Self { - query, - variables: VariablePaymentInput { - input: PaymentInput { - payment_method_id: connector_mandate_id.into(), - transaction: transaction_body, - }, - }, - }) - } -} - -impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> - for BraintreePaymentsRequest -{ - type Error = error_stack::Report; - fn try_from( - item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, - ) -> Result { - let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) - .change_context(errors::ConnectorError::InvalidConnectorConfig { - config: "metadata", - })?; - utils::validate_currency( - item.router_data.request.currency, - Some(metadata.merchant_config_currency), - )?; - match item.router_data.request.payment_method_data.clone() { - domain::PaymentMethodData::Card(_) => { - if item.router_data.is_three_ds() { - Ok(Self::CardThreeDs(BraintreeClientTokenRequest::try_from( - metadata, - )?)) - } else { - Ok(Self::Card(CardPaymentRequest::try_from((item, metadata))?)) - } - } - domain::PaymentMethodData::MandatePayment => { - let connector_mandate_id = item.router_data.request.connector_mandate_id().ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "connector_mandate_id", - }, - )?; - Ok(Self::Mandate(MandatePaymentRequest::try_from(( - item, - connector_mandate_id, - metadata, - ))?)) - } - domain::PaymentMethodData::CardRedirect(_) - | domain::PaymentMethodData::Wallet(_) - | domain::PaymentMethodData::PayLater(_) - | domain::PaymentMethodData::BankRedirect(_) - | domain::PaymentMethodData::BankDebit(_) - | domain::PaymentMethodData::BankTransfer(_) - | domain::PaymentMethodData::Crypto(_) - | domain::PaymentMethodData::Reward - | domain::PaymentMethodData::RealTimePayment(_) - | domain::PaymentMethodData::Upi(_) - | domain::PaymentMethodData::Voucher(_) - | domain::PaymentMethodData::GiftCard(_) - | domain::PaymentMethodData::OpenBanking(_) - | domain::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("braintree"), - ) - .into()) - } - } - } -} - -impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> - for BraintreePaymentsRequest -{ - type Error = error_stack::Report; - fn try_from( - item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, - ) -> Result { - match item.router_data.payment_method { - api_models::enums::PaymentMethod::Card => { - Ok(Self::Card(CardPaymentRequest::try_from(item)?)) - } - api_models::enums::PaymentMethod::CardRedirect - | api_models::enums::PaymentMethod::PayLater - | api_models::enums::PaymentMethod::Wallet - | api_models::enums::PaymentMethod::BankRedirect - | api_models::enums::PaymentMethod::BankTransfer - | api_models::enums::PaymentMethod::Crypto - | api_models::enums::PaymentMethod::BankDebit - | api_models::enums::PaymentMethod::Reward - | api_models::enums::PaymentMethod::RealTimePayment - | api_models::enums::PaymentMethod::Upi - | api_models::enums::PaymentMethod::Voucher - | api_models::enums::PaymentMethod::OpenBanking - | api_models::enums::PaymentMethod::GiftCard => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message( - "complete authorize flow", - ), - ) - .into()) - } - } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AuthResponse { - data: DataAuthResponse, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeAuthResponse { - AuthResponse(Box), - ClientTokenResponse(Box), - ErrorResponse(Box), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeCompleteAuthResponse { - AuthResponse(Box), - ErrorResponse(Box), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -struct PaymentMethodInfo { - id: Secret, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TransactionAuthChargeResponseBody { - id: String, - status: BraintreePaymentStatus, - payment_method: Option, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DataAuthResponse { - authorize_credit_card: AuthChargeCreditCard, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AuthChargeCreditCard { - transaction: TransactionAuthChargeResponseBody, -} - -impl - TryFrom< - types::ResponseRouterData< - F, - BraintreeAuthResponse, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - > for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData< - F, - BraintreeAuthResponse, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - ) -> Result { - match item.response { - BraintreeAuthResponse::ErrorResponse(error_response) => Ok(Self { - response: build_error_response(&error_response.errors, item.http_code), - ..item.data - }), - BraintreeAuthResponse::AuthResponse(auth_response) => { - let transaction_data = auth_response.data.authorize_credit_card.transaction; - - Ok(Self { - status: enums::AttemptStatus::from(transaction_data.status.clone()), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), - redirection_data: None, - mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { - MandateReference { - connector_mandate_id: Some(pm.id.clone().expose()), - payment_method_id: None, - } - }), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }) - } - BraintreeAuthResponse::ClientTokenResponse(client_token_data) => Ok(Self { - status: enums::AttemptStatus::AuthenticationPending, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::NoResponseId, - redirection_data: Some(get_braintree_redirect_form( - *client_token_data, - item.data.get_payment_method_token()?, - item.data.request.payment_method_data.clone(), - )?), - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }), - } - } -} - -fn build_error_response( - response: &[ErrorDetails], - http_code: u16, -) -> Result { - let error_messages = response - .iter() - .map(|error| error.message.to_string()) - .collect::>(); - - let reason = match !error_messages.is_empty() { - true => Some(error_messages.join(" ")), - false => None, - }; - - get_error_response( - response - .first() - .and_then(|err_details| err_details.extensions.as_ref()) - .and_then(|extensions| extensions.legacy_code.clone()), - response - .first() - .map(|err_details| err_details.message.clone()), - reason, - http_code, - ) -} - -fn get_error_response( - error_code: Option, - error_msg: Option, - error_reason: Option, - http_code: u16, -) -> Result { - Err(types::ErrorResponse { - code: error_code.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), - message: error_msg.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), - reason: error_reason, - status_code: http_code, - attempt_status: None, - connector_transaction_id: None, - }) -} - -// Using Auth type from braintree/transformer.rs, need this in later time when we use graphql version -// pub struct BraintreeAuthType { -// pub(super) auth_header: String, -// pub(super) merchant_id: Secret, -// } - -// impl TryFrom<&types::ConnectorAuthType> for BraintreeAuthType { -// type Error = error_stack::Report; -// fn try_from(item: &types::ConnectorAuthType) -> Result { -// if let types::ConnectorAuthType::SignatureKey { -// api_key: public_key, -// key1: merchant_id, -// api_secret: private_key, -// } = item -// { -// let auth_key = format!("{}:{}", public_key.peek(), private_key.peek()); -// let auth_header = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_key)); -// Ok(Self { -// auth_header, -// merchant_id: merchant_id.to_owned(), -// }) -// } else { -// Err(errors::ConnectorError::FailedToObtainAuthType)? -// } -// } -// } - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum BraintreePaymentStatus { - Authorized, - Authorizing, - AuthorizedExpired, - Failed, - ProcessorDeclined, - GatewayRejected, - Voided, - Settling, - Settled, - SettlementPending, - SettlementDeclined, - SettlementConfirmed, - SubmittedForSettlement, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ErrorDetails { - pub message: String, - pub extensions: Option, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AdditionalErrorDetails { - pub legacy_code: Option, -} - -impl From for enums::AttemptStatus { - fn from(item: BraintreePaymentStatus) -> Self { - match item { - BraintreePaymentStatus::Settling - | BraintreePaymentStatus::Settled - | BraintreePaymentStatus::SettlementConfirmed => Self::Charged, - BraintreePaymentStatus::Authorizing => Self::Authorizing, - BraintreePaymentStatus::AuthorizedExpired => Self::AuthorizationFailed, - BraintreePaymentStatus::Failed - | BraintreePaymentStatus::GatewayRejected - | BraintreePaymentStatus::ProcessorDeclined - | BraintreePaymentStatus::SettlementDeclined => Self::Failure, - BraintreePaymentStatus::Authorized => Self::Authorized, - BraintreePaymentStatus::Voided => Self::Voided, - BraintreePaymentStatus::SubmittedForSettlement - | BraintreePaymentStatus::SettlementPending => Self::Pending, - } - } -} - -impl - TryFrom< - types::ResponseRouterData< - F, - BraintreePaymentsResponse, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - > for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData< - F, - BraintreePaymentsResponse, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - ) -> Result { - match item.response { - BraintreePaymentsResponse::ErrorResponse(error_response) => Ok(Self { - response: build_error_response(&error_response.errors.clone(), item.http_code), - ..item.data - }), - BraintreePaymentsResponse::PaymentsResponse(payment_response) => { - let transaction_data = payment_response.data.charge_credit_card.transaction; - - Ok(Self { - status: enums::AttemptStatus::from(transaction_data.status.clone()), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), - redirection_data: None, - mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { - MandateReference { - connector_mandate_id: Some(pm.id.clone().expose()), - payment_method_id: None, - } - }), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }) - } - BraintreePaymentsResponse::ClientTokenResponse(client_token_data) => Ok(Self { - status: enums::AttemptStatus::AuthenticationPending, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::NoResponseId, - redirection_data: Some(get_braintree_redirect_form( - *client_token_data, - item.data.get_payment_method_token()?, - item.data.request.payment_method_data.clone(), - )?), - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }), - } - } -} - -impl - TryFrom< - types::ResponseRouterData< - F, - BraintreeCompleteChargeResponse, - types::CompleteAuthorizeData, - types::PaymentsResponseData, - >, - > for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData< - F, - BraintreeCompleteChargeResponse, - types::CompleteAuthorizeData, - types::PaymentsResponseData, - >, - ) -> Result { - match item.response { - BraintreeCompleteChargeResponse::ErrorResponse(error_response) => Ok(Self { - response: build_error_response(&error_response.errors.clone(), item.http_code), - ..item.data - }), - BraintreeCompleteChargeResponse::PaymentsResponse(payment_response) => { - let transaction_data = payment_response.data.charge_credit_card.transaction; - - Ok(Self { - status: enums::AttemptStatus::from(transaction_data.status.clone()), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), - redirection_data: None, - mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { - MandateReference { - connector_mandate_id: Some(pm.id.clone().expose()), - payment_method_id: None, - } - }), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }) - } - } - } -} - -impl - TryFrom< - types::ResponseRouterData< - F, - BraintreeCompleteAuthResponse, - types::CompleteAuthorizeData, - types::PaymentsResponseData, - >, - > for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData< - F, - BraintreeCompleteAuthResponse, - types::CompleteAuthorizeData, - types::PaymentsResponseData, - >, - ) -> Result { - match item.response { - BraintreeCompleteAuthResponse::ErrorResponse(error_response) => Ok(Self { - response: build_error_response(&error_response.errors, item.http_code), - ..item.data - }), - BraintreeCompleteAuthResponse::AuthResponse(auth_response) => { - let transaction_data = auth_response.data.authorize_credit_card.transaction; - - Ok(Self { - status: enums::AttemptStatus::from(transaction_data.status.clone()), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), - redirection_data: None, - mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { - MandateReference { - connector_mandate_id: Some(pm.id.clone().expose()), - payment_method_id: None, - } - }), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }) - } - } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PaymentsResponse { - data: DataResponse, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreePaymentsResponse { - PaymentsResponse(Box), - ClientTokenResponse(Box), - ErrorResponse(Box), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeCompleteChargeResponse { - PaymentsResponse(Box), - ErrorResponse(Box), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DataResponse { - charge_credit_card: AuthChargeCreditCard, -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RefundInputData { - amount: String, - merchant_account_id: Secret, -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BraintreeRefundInput { - transaction_id: String, - refund: RefundInputData, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct BraintreeRefundVariables { - input: BraintreeRefundInput, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct BraintreeRefundRequest { - query: String, - variables: BraintreeRefundVariables, -} - -impl TryFrom>> for BraintreeRefundRequest { - type Error = error_stack::Report; - fn try_from( - item: BraintreeRouterData<&types::RefundsRouterData>, - ) -> Result { - let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) - .change_context(errors::ConnectorError::InvalidConnectorConfig { - config: "metadata", - })?; - - utils::validate_currency( - item.router_data.request.currency, - Some(metadata.merchant_config_currency), - )?; - let query = REFUND_TRANSACTION_MUTATION.to_string(); - let variables = BraintreeRefundVariables { - input: BraintreeRefundInput { - transaction_id: item.router_data.request.connector_transaction_id.clone(), - refund: RefundInputData { - amount: item.amount, - merchant_account_id: metadata.merchant_account_id, - }, - }, - }; - Ok(Self { query, variables }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum BraintreeRefundStatus { - SettlementPending, - Settling, - Settled, - SubmittedForSettlement, - Failed, -} - -impl From for enums::RefundStatus { - fn from(item: BraintreeRefundStatus) -> Self { - match item { - BraintreeRefundStatus::Settled | BraintreeRefundStatus::Settling => Self::Success, - BraintreeRefundStatus::SubmittedForSettlement - | BraintreeRefundStatus::SettlementPending => Self::Pending, - BraintreeRefundStatus::Failed => Self::Failure, - } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct BraintreeRefundTransactionBody { - pub id: String, - pub status: BraintreeRefundStatus, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct BraintreeRefundTransaction { - pub refund: BraintreeRefundTransactionBody, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BraintreeRefundResponseData { - pub refund_transaction: BraintreeRefundTransaction, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] - -pub struct RefundResponse { - pub data: BraintreeRefundResponseData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeRefundResponse { - RefundResponse(Box), - ErrorResponse(Box), -} - -impl TryFrom> - for types::RefundsRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: match item.response { - BraintreeRefundResponse::ErrorResponse(error_response) => { - build_error_response(&error_response.errors, item.http_code) - } - BraintreeRefundResponse::RefundResponse(refund_data) => { - let refund_data = refund_data.data.refund_transaction.refund; - - Ok(types::RefundsResponseData { - connector_refund_id: refund_data.id.clone(), - refund_status: enums::RefundStatus::from(refund_data.status), - }) - } - }, - ..item.data - }) - } -} - -#[derive(Debug, Serialize)] -pub struct BraintreeRSyncRequest { - query: String, -} - -impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest { - type Error = error_stack::Report; - fn try_from(item: &types::RefundSyncRouterData) -> Result { - let metadata: BraintreeMeta = utils::to_connector_meta_from_secret( - item.connector_meta_data.clone(), - ) - .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata" })?; - utils::validate_currency( - item.request.currency, - Some(metadata.merchant_config_currency), - )?; - let refund_id = item.request.get_connector_refund_id()?; - let query = format!("query {{ search {{ refunds(input: {{ id: {{is: \"{}\"}} }}, first: 1) {{ edges {{ node {{ id status createdAt amount {{ value currencyCode }} orderId }} }} }} }} }}",refund_id); - - Ok(Self { query }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RSyncNodeData { - id: String, - status: BraintreeRefundStatus, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RSyncEdgeData { - node: RSyncNodeData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RefundData { - edges: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RSyncSearchData { - refunds: RefundData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RSyncResponseData { - search: RSyncSearchData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RSyncResponse { - data: RSyncResponseData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeRSyncResponse { - RSyncResponse(Box), - ErrorResponse(Box), -} - -impl TryFrom> - for types::RefundsRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::RefundsResponseRouterData, - ) -> Result { - match item.response { - BraintreeRSyncResponse::ErrorResponse(error_response) => Ok(Self { - response: build_error_response(&error_response.errors, item.http_code), - ..item.data - }), - BraintreeRSyncResponse::RSyncResponse(rsync_response) => { - let edge_data = rsync_response - .data - .search - .refunds - .edges - .first() - .ok_or(errors::ConnectorError::MissingConnectorRefundID)?; - let connector_refund_id = &edge_data.node.id; - let response = Ok(types::RefundsResponseData { - connector_refund_id: connector_refund_id.to_string(), - refund_status: enums::RefundStatus::from(edge_data.node.status.clone()), - }); - Ok(Self { - response, - ..item.data - }) - } - } - } -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreditCardData { - number: cards::CardNumber, - expiration_year: Secret, - expiration_month: Secret, - cvv: Secret, - cardholder_name: Secret, -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientTokenInput { - merchant_account_id: Secret, -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct InputData { - credit_card: CreditCardData, -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct InputClientTokenData { - client_token: ClientTokenInput, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct VariableInput { - input: InputData, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct VariableClientTokenInput { - input: InputClientTokenData, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct BraintreeTokenRequest { - query: String, - variables: VariableInput, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct BraintreeClientTokenRequest { - query: String, - variables: VariableClientTokenInput, -} - -impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { - type Error = error_stack::Report; - fn try_from(item: &types::TokenizationRouterData) -> Result { - match item.request.payment_method_data.clone() { - domain::PaymentMethodData::Card(card_data) => { - let query = TOKENIZE_CREDIT_CARD.to_string(); - let input = InputData { - credit_card: CreditCardData { - number: card_data.card_number, - expiration_year: card_data.card_exp_year, - expiration_month: card_data.card_exp_month, - cvv: card_data.card_cvc, - cardholder_name: item - .get_optional_billing_full_name() - .unwrap_or(Secret::new("".to_string())), - }, - }; - Ok(Self { - query, - variables: VariableInput { input }, - }) - } - domain::PaymentMethodData::CardRedirect(_) - | domain::PaymentMethodData::Wallet(_) - | domain::PaymentMethodData::PayLater(_) - | domain::PaymentMethodData::BankRedirect(_) - | domain::PaymentMethodData::BankDebit(_) - | domain::PaymentMethodData::BankTransfer(_) - | domain::PaymentMethodData::Crypto(_) - | domain::PaymentMethodData::MandatePayment - | domain::PaymentMethodData::Reward - | domain::PaymentMethodData::RealTimePayment(_) - | domain::PaymentMethodData::Upi(_) - | domain::PaymentMethodData::Voucher(_) - | domain::PaymentMethodData::GiftCard(_) - | domain::PaymentMethodData::OpenBanking(_) - | domain::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("braintree"), - ) - .into()) - } - } - } -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct TokenizePaymentMethodData { - id: Secret, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TokenizeCreditCardData { - payment_method: TokenizePaymentMethodData, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientToken { - client_token: Secret, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TokenizeCreditCard { - tokenize_credit_card: TokenizeCreditCardData, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientTokenData { - create_client_token: ClientToken, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct ClientTokenResponse { - data: ClientTokenData, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct TokenResponse { - data: TokenizeCreditCard, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct ErrorResponse { - errors: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeTokenResponse { - TokenResponse(Box), - ErrorResponse(Box), -} - -impl - TryFrom> - for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData, - ) -> Result { - Ok(Self { - response: match item.response { - BraintreeTokenResponse::ErrorResponse(error_response) => { - build_error_response(error_response.errors.as_ref(), item.http_code) - } - - BraintreeTokenResponse::TokenResponse(token_response) => { - Ok(types::PaymentsResponseData::TokenizationResponse { - token: token_response - .data - .tokenize_credit_card - .payment_method - .id - .expose() - .clone(), - }) - } - }, - ..item.data - }) - } -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CaptureTransactionBody { - amount: String, -} - -#[derive(Default, Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CaptureInputData { - transaction_id: String, - transaction: CaptureTransactionBody, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct VariableCaptureInput { - input: CaptureInputData, -} - -#[derive(Default, Debug, Clone, Serialize)] -pub struct BraintreeCaptureRequest { - query: String, - variables: VariableCaptureInput, -} - -impl TryFrom<&BraintreeRouterData<&types::PaymentsCaptureRouterData>> for BraintreeCaptureRequest { - type Error = error_stack::Report; - fn try_from( - item: &BraintreeRouterData<&types::PaymentsCaptureRouterData>, - ) -> Result { - let query = CAPTURE_TRANSACTION_MUTATION.to_string(); - let variables = VariableCaptureInput { - input: CaptureInputData { - transaction_id: item.router_data.request.connector_transaction_id.clone(), - transaction: CaptureTransactionBody { - amount: item.amount.to_owned(), - }, - }, - }; - Ok(Self { query, variables }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct CaptureResponseTransactionBody { - id: String, - status: BraintreePaymentStatus, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct CaptureTransactionData { - transaction: CaptureResponseTransactionBody, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CaptureResponseData { - capture_transaction: CaptureTransactionData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct CaptureResponse { - data: CaptureResponseData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeCaptureResponse { - CaptureResponse(Box), - ErrorResponse(Box), -} - -impl TryFrom> - for types::PaymentsCaptureRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::PaymentsCaptureResponseRouterData, - ) -> Result { - match item.response { - BraintreeCaptureResponse::CaptureResponse(capture_data) => { - let transaction_data = capture_data.data.capture_transaction.transaction; - - Ok(Self { - status: enums::AttemptStatus::from(transaction_data.status.clone()), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }) - } - BraintreeCaptureResponse::ErrorResponse(error_data) => Ok(Self { - response: build_error_response(&error_data.errors, item.http_code), - ..item.data - }), - } - } -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CancelInputData { - transaction_id: String, -} - -#[derive(Debug, Serialize)] -pub struct VariableCancelInput { - input: CancelInputData, -} - -#[derive(Debug, Serialize)] -pub struct BraintreeCancelRequest { - query: String, - variables: VariableCancelInput, -} - -impl TryFrom<&types::PaymentsCancelRouterData> for BraintreeCancelRequest { - type Error = error_stack::Report; - fn try_from(item: &types::PaymentsCancelRouterData) -> Result { - let query = VOID_TRANSACTION_MUTATION.to_string(); - let variables = VariableCancelInput { - input: CancelInputData { - transaction_id: item.request.connector_transaction_id.clone(), - }, - }; - Ok(Self { query, variables }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct CancelResponseTransactionBody { - id: String, - status: BraintreePaymentStatus, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct CancelTransactionData { - reversal: CancelResponseTransactionBody, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CancelResponseData { - reverse_transaction: CancelTransactionData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct CancelResponse { - data: CancelResponseData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreeCancelResponse { - CancelResponse(Box), - ErrorResponse(Box), -} - -impl - TryFrom> - for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData, - ) -> Result { - match item.response { - BraintreeCancelResponse::ErrorResponse(error_response) => Ok(Self { - response: build_error_response(&error_response.errors, item.http_code), - ..item.data - }), - BraintreeCancelResponse::CancelResponse(void_response) => { - let void_data = void_response.data.reverse_transaction.reversal; - - let transaction_id = void_data.id; - Ok(Self { - status: enums::AttemptStatus::from(void_data.status), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(transaction_id), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }) - } - } - } -} - -#[derive(Debug, Serialize)] -pub struct BraintreePSyncRequest { - query: String, -} - -impl TryFrom<&types::PaymentsSyncRouterData> for BraintreePSyncRequest { - type Error = error_stack::Report; - fn try_from(item: &types::PaymentsSyncRouterData) -> Result { - let transaction_id = item - .request - .connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; - let query = format!("query {{ search {{ transactions(input: {{ id: {{is: \"{}\"}} }}, first: 1) {{ edges {{ node {{ id status createdAt amount {{ value currencyCode }} orderId }} }} }} }} }}", transaction_id); - Ok(Self { query }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct NodeData { - id: String, - status: BraintreePaymentStatus, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct EdgeData { - node: NodeData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct TransactionData { - edges: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct SearchData { - transactions: TransactionData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PSyncResponseData { - search: SearchData, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum BraintreePSyncResponse { - PSyncResponse(Box), - ErrorResponse(Box), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PSyncResponse { - data: PSyncResponseData, -} - -impl - TryFrom> - for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::ResponseRouterData, - ) -> Result { - match item.response { - BraintreePSyncResponse::ErrorResponse(error_response) => Ok(Self { - response: build_error_response(&error_response.errors, item.http_code), - ..item.data - }), - BraintreePSyncResponse::PSyncResponse(psync_response) => { - let edge_data = psync_response - .data - .search - .transactions - .edges - .first() - .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; - let transaction_id = &edge_data.node.id; - Ok(Self { - status: enums::AttemptStatus::from(edge_data.node.status.clone()), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - transaction_id.to_string(), - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), - ..item.data - }) - } - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BraintreeThreeDsResponse { - pub nonce: Secret, - pub liability_shifted: bool, - pub liability_shift_possible: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BraintreeThreeDsErrorResponse { - pub code: String, - pub message: String, -} - -#[derive(Debug, Deserialize)] -pub struct BraintreeRedirectionResponse { - pub authentication_response: String, -} - -impl TryFrom for BraintreeClientTokenRequest { - type Error = error_stack::Report; - fn try_from(metadata: BraintreeMeta) -> Result { - Ok(Self { - query: CLIENT_TOKEN_MUTATION.to_owned(), - variables: VariableClientTokenInput { - input: InputClientTokenData { - client_token: ClientTokenInput { - merchant_account_id: metadata.merchant_account_id, - }, - }, - }, - }) - } -} - -impl - TryFrom<( - &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, - BraintreeMeta, - )> for CardPaymentRequest -{ - type Error = error_stack::Report; - fn try_from( - (item, metadata): ( - &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, - BraintreeMeta, - ), - ) -> Result { - let (query, transaction_body) = if item.router_data.request.is_mandate_payment() { - ( - match item.router_data.request.is_auto_capture()? { - true => CHARGE_AND_VAULT_TRANSACTION_MUTATION.to_string(), - false => AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION.to_string(), - }, - TransactionBody::Vault(VaultTransactionBody { - amount: item.amount.to_owned(), - merchant_account_id: metadata.merchant_account_id, - vault_payment_method_after_transacting: TransactionTiming { - when: "ALWAYS".to_string(), - }, - }), - ) - } else { - ( - match item.router_data.request.is_auto_capture()? { - true => CHARGE_CREDIT_CARD_MUTATION.to_string(), - false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(), - }, - TransactionBody::Regular(RegularTransactionBody { - amount: item.amount.to_owned(), - merchant_account_id: metadata.merchant_account_id, - }), - ) - }; - Ok(Self { - query, - variables: VariablePaymentInput { - input: PaymentInput { - payment_method_id: match item.router_data.get_payment_method_token()? { - types::PaymentMethodToken::Token(token) => token, - types::PaymentMethodToken::ApplePayDecrypt(_) => Err( - unimplemented_payment_method!("Apple Pay", "Simplified", "Braintree"), - )?, - }, - transaction: transaction_body, - }, - }, - }) - } -} - -impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> - for CardPaymentRequest -{ - type Error = error_stack::Report; - fn try_from( - item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, - ) -> Result { - let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) - .change_context(errors::ConnectorError::InvalidConnectorConfig { - config: "metadata", - })?; - utils::validate_currency( - item.router_data.request.currency, - Some(metadata.merchant_config_currency), - )?; - let payload_data = PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload( - &item.router_data.request, - )? - .expose(); - let redirection_response: BraintreeRedirectionResponse = serde_json::from_value( - payload_data, - ) - .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { - field_name: "redirection_response", - })?; - let three_ds_data = serde_json::from_str::( - &redirection_response.authentication_response, - ) - .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { - field_name: "three_ds_data", - })?; - - let (query, transaction_body) = if item.router_data.request.is_mandate_payment() { - ( - match item.router_data.request.is_auto_capture()? { - true => CHARGE_AND_VAULT_TRANSACTION_MUTATION.to_string(), - false => AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION.to_string(), - }, - TransactionBody::Vault(VaultTransactionBody { - amount: item.amount.to_owned(), - merchant_account_id: metadata.merchant_account_id, - vault_payment_method_after_transacting: TransactionTiming { - when: "ALWAYS".to_string(), - }, - }), - ) - } else { - ( - match item.router_data.request.is_auto_capture()? { - true => CHARGE_CREDIT_CARD_MUTATION.to_string(), - false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(), - }, - TransactionBody::Regular(RegularTransactionBody { - amount: item.amount.to_owned(), - merchant_account_id: metadata.merchant_account_id, - }), - ) - }; - Ok(Self { - query, - variables: VariablePaymentInput { - input: PaymentInput { - payment_method_id: three_ds_data.nonce, - transaction: transaction_body, - }, - }, - }) - } -} - -fn get_braintree_redirect_form( - client_token_data: ClientTokenResponse, - payment_method_token: types::PaymentMethodToken, - card_details: domain::PaymentMethodData, -) -> Result> { - Ok(services::RedirectForm::Braintree { - client_token: client_token_data - .data - .create_client_token - .client_token - .expose(), - card_token: match payment_method_token { - types::PaymentMethodToken::Token(token) => token.expose(), - types::PaymentMethodToken::ApplePayDecrypt(_) => Err(unimplemented_payment_method!( - "Apple Pay", - "Simplified", - "Braintree" - ))?, - }, - bin: match card_details { - domain::PaymentMethodData::Card(card_details) => { - card_details.card_number.get_card_isin() - } - domain::PaymentMethodData::CardRedirect(_) - | domain::PaymentMethodData::Wallet(_) - | domain::PaymentMethodData::PayLater(_) - | domain::PaymentMethodData::BankRedirect(_) - | domain::PaymentMethodData::BankDebit(_) - | domain::PaymentMethodData::BankTransfer(_) - | domain::PaymentMethodData::Crypto(_) - | domain::PaymentMethodData::MandatePayment - | domain::PaymentMethodData::Reward - | domain::PaymentMethodData::RealTimePayment(_) - | domain::PaymentMethodData::Upi(_) - | domain::PaymentMethodData::Voucher(_) - | domain::PaymentMethodData::GiftCard(_) - | domain::PaymentMethodData::OpenBanking(_) - | domain::PaymentMethodData::CardToken(_) => Err( - errors::ConnectorError::NotImplemented("given payment method".to_owned()), - )?, - }, - }) -} - -#[derive(Debug, Deserialize)] -pub struct BraintreeWebhookResponse { - pub bt_signature: String, - pub bt_payload: String, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Notification { - pub kind: String, // xml parse only string to fields - pub timestamp: String, - pub dispute: Option, -} -impl types::transformers::ForeignFrom<&str> for api_models::webhooks::IncomingWebhookEvent { - fn foreign_from(status: &str) -> Self { - match status { - "dispute_opened" => Self::DisputeOpened, - "dispute_lost" => Self::DisputeLost, - "dispute_won" => Self::DisputeWon, - "dispute_accepted" | "dispute_auto_accepted" => Self::DisputeAccepted, - "dispute_expired" => Self::DisputeExpired, - "dispute_disputed" => Self::DisputeChallenged, - _ => Self::EventNotSupported, - } - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct BraintreeDisputeData { - pub amount_disputed: i64, - pub amount_won: Option, - pub case_number: Option, - pub chargeback_protection_level: Option, - pub currency_iso_code: String, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub created_at: Option, - pub evidence: Option, - pub id: String, - pub kind: String, // xml parse only string to fields - pub status: String, - pub reason: Option, - pub reason_code: Option, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub updated_at: Option, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub reply_by_date: Option, - pub transaction: DisputeTransaction, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct DisputeTransaction { - pub amount: String, - pub id: String, -} -#[derive(Debug, Deserialize, Serialize)] -pub struct DisputeEvidence { - pub comment: String, - pub id: Secret, - pub created_at: Option, - pub url: url::Url, -} - -pub(crate) fn get_dispute_stage(code: &str) -> Result { - match code { - "CHARGEBACK" => Ok(enums::DisputeStage::Dispute), - "PRE_ARBITATION" => Ok(enums::DisputeStage::PreArbitration), - "RETRIEVAL" => Ok(enums::DisputeStage::PreDispute), - _ => Err(errors::ConnectorError::WebhookBodyDecodingFailed), - } -} diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index 717c4afc1956..8a4ce8126dd8 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -1,175 +1,308 @@ -use api_models::payments; -use base64::Engine; -use masking::{ExposeInterface, PeekInterface, Secret}; +use common_utils::{pii, types::StringMajorUnit}; +use error_stack::ResultExt; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use crate::{ - connector::utils::{self}, + connector::utils::{ + self, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData, + RefundsRequestData, RouterData, + }, consts, core::errors, - types::{self, api, domain, storage::enums}, + services, + types::{self, api, domain, storage::enums, MandateReference}, + unimplemented_payment_method, }; +pub const CHANNEL_CODE: &str = "HyperSwitchBT_Ecom"; +pub const CLIENT_TOKEN_MUTATION: &str = "mutation createClientToken($input: CreateClientTokenInput!) { createClientToken(input: $input) { clientToken}}"; +pub const TOKENIZE_CREDIT_CARD: &str = "mutation tokenizeCreditCard($input: TokenizeCreditCardInput!) { tokenizeCreditCard(input: $input) { clientMutationId paymentMethod { id } } }"; +pub const CHARGE_CREDIT_CARD_MUTATION: &str = "mutation ChargeCreditCard($input: ChargeCreditCardInput!) { chargeCreditCard(input: $input) { transaction { id legacyId createdAt amount { value currencyCode } status } } }"; +pub const AUTHORIZE_CREDIT_CARD_MUTATION: &str = "mutation authorizeCreditCard($input: AuthorizeCreditCardInput!) { authorizeCreditCard(input: $input) { transaction { id legacyId amount { value currencyCode } status } } }"; +pub const CAPTURE_TRANSACTION_MUTATION: &str = "mutation captureTransaction($input: CaptureTransactionInput!) { captureTransaction(input: $input) { clientMutationId transaction { id legacyId amount { value currencyCode } status } } }"; +pub const VOID_TRANSACTION_MUTATION: &str = "mutation voidTransaction($input: ReverseTransactionInput!) { reverseTransaction(input: $input) { clientMutationId reversal { ... on Transaction { id legacyId amount { value currencyCode } status } } } }"; +pub const REFUND_TRANSACTION_MUTATION: &str = "mutation refundTransaction($input: RefundTransactionInput!) { refundTransaction(input: $input) {clientMutationId refund { id legacyId amount { value currencyCode } status } } }"; +pub const AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION: &str="mutation authorizeCreditCard($input: AuthorizeCreditCardInput!) { authorizeCreditCard(input: $input) { transaction { id status createdAt paymentMethod { id } } } }"; +pub const CHARGE_AND_VAULT_TRANSACTION_MUTATION: &str ="mutation ChargeCreditCard($input: ChargeCreditCardInput!) { chargeCreditCard(input: $input) { transaction { id status createdAt paymentMethod { id } } } }"; +pub const TRANSACTION_QUERY: &str = "query($input: TransactionSearchInput!) { search { transactions(input: $input) { edges { node { id status } } } } }"; +pub const REFUND_QUERY: &str = "query($input: RefundSearchInput!) { search { refunds(input: $input, first: 1) { edges { node { id status createdAt amount { value currencyCode } orderId } } } } }"; -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct DeviceData; +pub type CardPaymentRequest = GenericBraintreeRequest; +pub type MandatePaymentRequest = GenericBraintreeRequest; +pub type BraintreeClientTokenRequest = GenericBraintreeRequest; +pub type BraintreeTokenRequest = GenericBraintreeRequest; +pub type BraintreeCaptureRequest = GenericBraintreeRequest; +pub type BraintreeRefundRequest = GenericBraintreeRequest; +pub type BraintreePSyncRequest = GenericBraintreeRequest; +pub type BraintreeRSyncRequest = GenericBraintreeRequest; -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct PaymentOptions { - submit_for_settlement: bool, +pub type BraintreeRefundResponse = GenericBraintreeResponse; +pub type BraintreeCaptureResponse = GenericBraintreeResponse; +pub type BraintreePSyncResponse = GenericBraintreeResponse; + +pub type VariablePaymentInput = GenericVariableInput; +pub type VariableClientTokenInput = GenericVariableInput; +pub type VariableInput = GenericVariableInput; +pub type VariableCaptureInput = GenericVariableInput; +pub type BraintreeRefundVariables = GenericVariableInput; +pub type PSyncInput = GenericVariableInput; +pub type RSyncInput = GenericVariableInput; + +#[derive(Debug, Clone, Serialize)] +pub struct GenericBraintreeRequest { + query: String, + variables: T, +} +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum GenericBraintreeResponse { + SuccessResponse(Box), + ErrorResponse(Box), +} +#[derive(Debug, Clone, Serialize)] +pub struct GenericVariableInput { + input: T, } -#[derive(Debug, Deserialize)] -pub struct BraintreeMeta { - merchant_account_id: Option>, - merchant_config_currency: Option, +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] + +pub struct BraintreeApiErrorResponse { + pub api_error_response: ApiErrorResponse, } -#[derive(Debug, Serialize, Eq, PartialEq)] -pub struct BraintreePaymentsRequest { - transaction: TransactionBody, +#[derive(Debug, Deserialize, Serialize)] + +pub struct ErrorsObject { + pub errors: Vec, + + pub transaction: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BraintreeApiVersion { - version: String, +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] + +pub struct TransactionError { + pub errors: Vec, + pub credit_card: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct BraintreeSessionRequest { - client_token: BraintreeApiVersion, +#[derive(Debug, Deserialize, Serialize)] +pub struct CreditCardError { + pub errors: Vec, +} +#[derive(Debug, Serialize)] +pub struct BraintreeRouterData { + pub amount: StringMajorUnit, + pub router_data: T, } -impl TryFrom<&types::PaymentsSessionRouterData> for BraintreeSessionRequest { +impl TryFrom<(StringMajorUnit, T)> for BraintreeRouterData { type Error = error_stack::Report; - fn try_from(_item: &types::PaymentsSessionRouterData) -> Result { - let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(_item.connector_meta_data.clone())?; - - utils::validate_currency(_item.request.currency, metadata.merchant_config_currency)?; + fn try_from((amount, item): (StringMajorUnit, T)) -> Result { Ok(Self { - client_token: BraintreeApiVersion { - version: "2".to_string(), - }, + amount, + router_data: item, }) } } +#[derive(Debug, Deserialize, Serialize)] +pub struct ErrorObject { + pub code: String, + pub message: String, +} -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct TransactionBody { - amount: String, - merchant_account_id: Option>, - device_data: DeviceData, - options: PaymentOptions, - #[serde(flatten)] - payment_method_data_type: PaymentMethodType, - #[serde(rename = "type")] - kind: String, -} - -#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct BraintreeErrorResponse { + pub errors: String, +} + +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -#[serde(tag = "type")] -pub enum PaymentMethodType { - CreditCard(Card), - PaymentMethodNonce(Nonce), +#[serde(untagged)] + +pub enum ErrorResponses { + BraintreeApiErrorResponse(Box), + BraintreeErrorResponse(Box), } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct Nonce { - payment_method_nonce: Secret, +#[derive(Debug, Deserialize, Serialize)] +pub struct ApiErrorResponse { + pub message: String, + pub errors: ErrorsObject, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct Card { - credit_card: CardDetails, +pub struct BraintreeAuthType { + pub(super) public_key: Secret, + pub(super) private_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for BraintreeAuthType { + type Error = error_stack::Report; + + fn try_from(item: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::SignatureKey { + api_key, + api_secret, + key1: _merchant_id, + } = item + { + Ok(Self { + public_key: api_key.to_owned(), + private_key: api_secret.to_owned(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? + } + } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct CardDetails { - number: cards::CardNumber, - expiration_month: Secret, - expiration_year: Secret, - cvv: Secret, +pub struct PaymentInput { + payment_method_id: Secret, + transaction: TransactionBody, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum BraintreePaymentsRequest { + Card(CardPaymentRequest), + CardThreeDs(BraintreeClientTokenRequest), + Mandate(MandatePaymentRequest), +} + +#[derive(Debug, Deserialize)] +pub struct BraintreeMeta { + merchant_account_id: Secret, + merchant_config_currency: enums::Currency, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest { +impl TryFrom<&Option> for BraintreeMeta { type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RegularTransactionBody { + amount: StringMajorUnit, + merchant_account_id: Secret, + channel: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultTransactionBody { + amount: StringMajorUnit, + merchant_account_id: Secret, + vault_payment_method_after_transacting: TransactionTiming, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum TransactionBody { + Regular(RegularTransactionBody), + Vault(VaultTransactionBody), +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionTiming { + when: String, +} - utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?; - let submit_for_settlement = matches!( - item.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None +impl + TryFrom<( + &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, + String, + BraintreeMeta, + )> for MandatePaymentRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, connector_mandate_id, metadata): ( + &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, + String, + BraintreeMeta, + ), + ) -> Result { + let (query, transaction_body) = ( + match item.router_data.request.is_auto_capture()? { + true => CHARGE_CREDIT_CARD_MUTATION.to_string(), + false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(), + }, + TransactionBody::Regular(RegularTransactionBody { + amount: item.amount.to_owned(), + merchant_account_id: metadata.merchant_account_id, + channel: CHANNEL_CODE.to_string(), + }), ); - let merchant_account_id = metadata.merchant_account_id; - let amount = utils::to_currency_base_unit(item.request.amount, item.request.currency)?; - let device_data = DeviceData {}; - let options = PaymentOptions { - submit_for_settlement, - }; - let kind = "sale".to_string(); - - let payment_method_data_type = match item.request.payment_method_data.clone() { - domain::PaymentMethodData::Card(ccard) => Ok(PaymentMethodType::CreditCard(Card { - credit_card: CardDetails { - number: ccard.card_number, - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - cvv: ccard.card_cvc, + Ok(Self { + query, + variables: VariablePaymentInput { + input: PaymentInput { + payment_method_id: connector_mandate_id.into(), + transaction: transaction_body, }, - })), - domain::PaymentMethodData::Wallet(ref wallet_data) => { - Ok(PaymentMethodType::PaymentMethodNonce(Nonce { - payment_method_nonce: match wallet_data { - domain::WalletData::PaypalSdk(wallet_data) => { - Ok(wallet_data.token.to_owned()) - } - domain::WalletData::ApplePay(_) - | domain::WalletData::GooglePay(_) - | domain::WalletData::SamsungPay(_) - | domain::WalletData::AliPayQr(_) - | domain::WalletData::AliPayRedirect(_) - | domain::WalletData::AliPayHkRedirect(_) - | domain::WalletData::MomoRedirect(_) - | domain::WalletData::KakaoPayRedirect(_) - | domain::WalletData::GoPayRedirect(_) - | domain::WalletData::GcashRedirect(_) - | domain::WalletData::ApplePayRedirect(_) - | domain::WalletData::ApplePayThirdPartySdk(_) - | domain::WalletData::DanaRedirect {} - | domain::WalletData::GooglePayRedirect(_) - | domain::WalletData::GooglePayThirdPartySdk(_) - | domain::WalletData::MbWayRedirect(_) - | domain::WalletData::MobilePayRedirect(_) - | domain::WalletData::PaypalRedirect(_) - | domain::WalletData::TwintRedirect {} - | domain::WalletData::VippsRedirect {} - | domain::WalletData::TouchNGoRedirect(_) - | domain::WalletData::WeChatPayRedirect(_) - | domain::WalletData::WeChatPayQr(_) - | domain::WalletData::CashappQr(_) - | domain::WalletData::SwishQr(_) - | domain::WalletData::Mifinity(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("braintree"), - )) - } - }? - .into(), - })) + }, + }) + } +} + +impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> + for BraintreePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + let metadata: BraintreeMeta = + utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + utils::validate_currency( + item.router_data.request.currency, + Some(metadata.merchant_config_currency), + )?; + match item.router_data.request.payment_method_data.clone() { + domain::PaymentMethodData::Card(_) => { + if item.router_data.is_three_ds() { + Ok(Self::CardThreeDs(BraintreeClientTokenRequest::try_from( + metadata, + )?)) + } else { + Ok(Self::Card(CardPaymentRequest::try_from((item, metadata))?)) + } + } + domain::PaymentMethodData::MandatePayment => { + let connector_mandate_id = item.router_data.request.connector_mandate_id().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_id", + }, + )?; + Ok(Self::Mandate(MandatePaymentRequest::try_from(( + item, + connector_mandate_id, + metadata, + ))?)) } - domain::PaymentMethodData::PayLater(_) + domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::Wallet(_) + | domain::PaymentMethodData::PayLater(_) | domain::PaymentMethodData::BankRedirect(_) | domain::PaymentMethodData::BankDebit(_) | domain::PaymentMethodData::BankTransfer(_) | domain::PaymentMethodData::Crypto(_) - | domain::PaymentMethodData::CardRedirect(_) - | domain::PaymentMethodData::MandatePayment | domain::PaymentMethodData::Reward | domain::PaymentMethodData::RealTimePayment(_) | domain::PaymentMethodData::Upi(_) @@ -179,74 +312,253 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest { | domain::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("braintree"), - )) + ) + .into()) } - }?; - let braintree_transaction_body = TransactionBody { - amount, - merchant_account_id, - device_data, - options, - payment_method_data_type, - kind, - }; - Ok(Self { - transaction: braintree_transaction_body, - }) + } } } -pub struct BraintreeAuthType { - pub(super) auth_header: String, - pub(super) merchant_id: Secret, +impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for BraintreePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + match item.router_data.payment_method { + api_models::enums::PaymentMethod::Card => { + Ok(Self::Card(CardPaymentRequest::try_from(item)?)) + } + api_models::enums::PaymentMethod::CardRedirect + | api_models::enums::PaymentMethod::PayLater + | api_models::enums::PaymentMethod::Wallet + | api_models::enums::PaymentMethod::BankRedirect + | api_models::enums::PaymentMethod::BankTransfer + | api_models::enums::PaymentMethod::Crypto + | api_models::enums::PaymentMethod::BankDebit + | api_models::enums::PaymentMethod::Reward + | api_models::enums::PaymentMethod::RealTimePayment + | api_models::enums::PaymentMethod::Upi + | api_models::enums::PaymentMethod::OpenBanking + | api_models::enums::PaymentMethod::Voucher + | api_models::enums::PaymentMethod::GiftCard => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message( + "complete authorize flow", + ), + ) + .into()) + } + } + } } -impl TryFrom<&types::ConnectorAuthType> for BraintreeAuthType { +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AuthResponse { + data: DataAuthResponse, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum BraintreeAuthResponse { + AuthResponse(Box), + ClientTokenResponse(Box), + ErrorResponse(Box), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum BraintreeCompleteAuthResponse { + AuthResponse(Box), + ErrorResponse(Box), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct PaymentMethodInfo { + id: Secret, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionAuthChargeResponseBody { + id: String, + status: BraintreePaymentStatus, + payment_method: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DataAuthResponse { + authorize_credit_card: AuthChargeCreditCard, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AuthChargeCreditCard { + transaction: TransactionAuthChargeResponseBody, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BraintreeAuthResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ type Error = error_stack::Report; - fn try_from(item: &types::ConnectorAuthType) -> Result { - if let types::ConnectorAuthType::SignatureKey { - api_key: public_key, - key1: merchant_id, - api_secret: private_key, - } = item - { - let auth_key = format!("{}:{}", public_key.peek(), private_key.peek()); - let auth_header = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_key)); - Ok(Self { - auth_header, - merchant_id: merchant_id.to_owned(), - }) - } else { - Err(errors::ConnectorError::FailedToObtainAuthType)? + fn try_from( + item: types::ResponseRouterData< + F, + BraintreeAuthResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BraintreeAuthResponse::ErrorResponse(error_response) => Ok(Self { + response: build_error_response(&error_response.errors, item.http_code), + ..item.data + }), + BraintreeAuthResponse::AuthResponse(auth_response) => { + let transaction_data = auth_response.data.authorize_credit_card.transaction; + let status = enums::AttemptStatus::from(transaction_data.status.clone()); + let response = if utils::is_payment_failure(status) { + Err(types::ErrorResponse { + code: transaction_data.status.to_string().clone(), + message: transaction_data.status.to_string().clone(), + reason: Some(transaction_data.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: Some(transaction_data.id), + status_code: item.http_code, + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), + redirection_data: None, + mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { + MandateReference { + connector_mandate_id: Some(pm.id.clone().expose()), + payment_method_id: None, + } + }), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } + BraintreeAuthResponse::ClientTokenResponse(client_token_data) => Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(get_braintree_redirect_form( + *client_token_data, + item.data.get_payment_method_token()?, + item.data.request.payment_method_data.clone(), + )?), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }), } } } -#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] +fn build_error_response( + response: &[ErrorDetails], + http_code: u16, +) -> Result { + let error_messages = response + .iter() + .map(|error| error.message.to_string()) + .collect::>(); + + let reason = match !error_messages.is_empty() { + true => Some(error_messages.join(" ")), + false => None, + }; + + get_error_response( + response + .first() + .and_then(|err_details| err_details.extensions.as_ref()) + .and_then(|extensions| extensions.legacy_code.clone()), + response + .first() + .map(|err_details| err_details.message.clone()), + reason, + http_code, + ) +} + +fn get_error_response( + error_code: Option, + error_msg: Option, + error_reason: Option, + http_code: u16, +) -> Result { + Err(types::ErrorResponse { + code: error_code.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: error_msg.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: error_reason, + status_code: http_code, + attempt_status: None, + connector_transaction_id: None, + }) +} + +#[derive(Debug, Clone, Deserialize, Serialize, strum::Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BraintreePaymentStatus { - Succeeded, - Failed, Authorized, + Authorizing, AuthorizedExpired, + Failed, ProcessorDeclined, GatewayRejected, Voided, - SubmittedForSettlement, - #[default] Settling, Settled, SettlementPending, SettlementDeclined, SettlementConfirmed, + SubmittedForSettlement, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ErrorDetails { + pub message: String, + pub extensions: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalErrorDetails { + pub legacy_code: Option, } impl From for enums::AttemptStatus { fn from(item: BraintreePaymentStatus) -> Self { match item { - BraintreePaymentStatus::Succeeded - | BraintreePaymentStatus::Settling - | BraintreePaymentStatus::Settled => Self::Charged, + BraintreePaymentStatus::Settling + | BraintreePaymentStatus::Settled + | BraintreePaymentStatus::SettlementConfirmed => Self::Charged, + BraintreePaymentStatus::Authorizing => Self::Authorizing, BraintreePaymentStatus::AuthorizedExpired => Self::AuthorizationFailed, BraintreePaymentStatus::Failed | BraintreePaymentStatus::GatewayRejected @@ -255,234 +567,1221 @@ impl From for enums::AttemptStatus { BraintreePaymentStatus::Authorized => Self::Authorized, BraintreePaymentStatus::Voided => Self::Voided, BraintreePaymentStatus::SubmittedForSettlement - | BraintreePaymentStatus::SettlementPending - | BraintreePaymentStatus::SettlementConfirmed => Self::Pending, + | BraintreePaymentStatus::SettlementPending => Self::Pending, } } } -impl - TryFrom> - for types::RouterData +impl + TryFrom< + types::ResponseRouterData< + F, + BraintreePaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, BraintreePaymentsResponse, - T, + types::PaymentsAuthorizeData, types::PaymentsResponseData, >, ) -> Result { - let id = item.response.transaction.id.clone(); - Ok(Self { - status: enums::AttemptStatus::from(item.response.transaction.status), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(id.clone()), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: Some(id), - incremental_authorization_allowed: None, - charge_id: None, + match item.response { + BraintreePaymentsResponse::ErrorResponse(error_response) => Ok(Self { + response: build_error_response(&error_response.errors.clone(), item.http_code), + ..item.data }), - ..item.data - }) + BraintreePaymentsResponse::PaymentsResponse(payment_response) => { + let transaction_data = payment_response.data.charge_credit_card.transaction; + let status = enums::AttemptStatus::from(transaction_data.status.clone()); + let response = if utils::is_payment_failure(status) { + Err(types::ErrorResponse { + code: transaction_data.status.to_string().clone(), + message: transaction_data.status.to_string().clone(), + reason: Some(transaction_data.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: Some(transaction_data.id), + status_code: item.http_code, + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), + redirection_data: None, + mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { + MandateReference { + connector_mandate_id: Some(pm.id.clone().expose()), + payment_method_id: None, + } + }), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } + BraintreePaymentsResponse::ClientTokenResponse(client_token_data) => Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(get_braintree_redirect_form( + *client_token_data, + item.data.get_payment_method_token()?, + item.data.request.payment_method_data.clone(), + )?), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }), + } } } -impl +impl TryFrom< - types::ResponseRouterData, - > for types::RouterData + types::ResponseRouterData< + F, + BraintreeCompleteChargeResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, - BraintreeSessionTokenResponse, - T, + BraintreeCompleteChargeResponse, + types::CompleteAuthorizeData, types::PaymentsResponseData, >, ) -> Result { - Ok(Self { - response: Ok(types::PaymentsResponseData::SessionResponse { - session_token: api::SessionToken::Paypal(Box::new( - payments::PaypalSessionTokenResponse { - session_token: item.response.client_token.value.expose(), - connector: "braintree".to_string(), - sdk_next_action: payments::SdkNextAction { - next_action: payments::NextActionCall::Confirm, - }, - }, - )), + match item.response { + BraintreeCompleteChargeResponse::ErrorResponse(error_response) => Ok(Self { + response: build_error_response(&error_response.errors.clone(), item.http_code), + ..item.data }), - ..item.data - }) + BraintreeCompleteChargeResponse::PaymentsResponse(payment_response) => { + let transaction_data = payment_response.data.charge_credit_card.transaction; + let status = enums::AttemptStatus::from(transaction_data.status.clone()); + let response = if utils::is_payment_failure(status) { + Err(types::ErrorResponse { + code: transaction_data.status.to_string().clone(), + message: transaction_data.status.to_string().clone(), + reason: Some(transaction_data.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: Some(transaction_data.id), + status_code: item.http_code, + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), + redirection_data: None, + mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { + MandateReference { + connector_mandate_id: Some(pm.id.clone().expose()), + payment_method_id: None, + } + }), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } + } } } -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BraintreePaymentsResponse { - transaction: TransactionResponse, +impl + TryFrom< + types::ResponseRouterData< + F, + BraintreeCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BraintreeCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BraintreeCompleteAuthResponse::ErrorResponse(error_response) => Ok(Self { + response: build_error_response(&error_response.errors, item.http_code), + ..item.data + }), + BraintreeCompleteAuthResponse::AuthResponse(auth_response) => { + let transaction_data = auth_response.data.authorize_credit_card.transaction; + let status = enums::AttemptStatus::from(transaction_data.status.clone()); + let response = if utils::is_payment_failure(status) { + Err(types::ErrorResponse { + code: transaction_data.status.to_string().clone(), + message: transaction_data.status.to_string().clone(), + reason: Some(transaction_data.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: Some(transaction_data.id), + status_code: item.http_code, + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), + redirection_data: None, + mandate_reference: transaction_data.payment_method.as_ref().map(|pm| { + MandateReference { + connector_mandate_id: Some(pm.id.clone().expose()), + payment_method_id: None, + } + }), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } + } + } } -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientToken { - pub value: Secret, +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PaymentsResponse { + data: DataResponse, } -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BraintreeSessionTokenResponse { - pub client_token: ClientToken, +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum BraintreePaymentsResponse { + PaymentsResponse(Box), + ClientTokenResponse(Box), + ErrorResponse(Box), } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum BraintreeCompleteChargeResponse { + PaymentsResponse(Box), + ErrorResponse(Box), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct TransactionResponse { - id: String, - currency_iso_code: String, - amount: String, - status: BraintreePaymentStatus, +pub struct DataResponse { + charge_credit_card: AuthChargeCreditCard, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct BraintreeApiErrorResponse { - pub api_error_response: ApiErrorResponse, +pub struct RefundInputData { + amount: StringMajorUnit, + merchant_account_id: Secret, +} +#[derive(Serialize, Debug, Clone)] +struct IdFilter { + is: String, } -#[derive(Debug, Deserialize, Serialize)] -pub struct ErrorsObject { - pub errors: Vec, - pub transaction: Option, +#[derive(Debug, Clone, Serialize)] +pub struct TransactionSearchInput { + id: IdFilter, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct TransactionError { - pub errors: Vec, - pub credit_card: Option, +pub struct BraintreeRefundInput { + transaction_id: String, + refund: RefundInputData, } -#[derive(Debug, Deserialize, Serialize)] -pub struct CreditCardError { - pub errors: Vec, +impl TryFrom>> for BraintreeRefundRequest { + type Error = error_stack::Report; + fn try_from( + item: BraintreeRouterData<&types::RefundsRouterData>, + ) -> Result { + let metadata: BraintreeMeta = + utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + + utils::validate_currency( + item.router_data.request.currency, + Some(metadata.merchant_config_currency), + )?; + let query = REFUND_TRANSACTION_MUTATION.to_string(); + let variables = BraintreeRefundVariables { + input: BraintreeRefundInput { + transaction_id: item.router_data.request.connector_transaction_id.clone(), + refund: RefundInputData { + amount: item.amount, + merchant_account_id: metadata.merchant_account_id, + }, + }, + }; + Ok(Self { query, variables }) + } } -#[derive(Debug, Deserialize, Serialize)] -pub struct ErrorObject { - pub code: String, - pub message: String, +#[derive(Debug, Clone, Deserialize, Serialize, strum::Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BraintreeRefundStatus { + SettlementPending, + Settling, + Settled, + SubmittedForSettlement, + Failed, } -#[derive(Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BraintreeErrorResponse { - pub errors: String, +impl From for enums::RefundStatus { + fn from(item: BraintreeRefundStatus) -> Self { + match item { + BraintreeRefundStatus::Settled | BraintreeRefundStatus::Settling => Self::Success, + BraintreeRefundStatus::SubmittedForSettlement + | BraintreeRefundStatus::SettlementPending => Self::Pending, + BraintreeRefundStatus::Failed => Self::Failure, + } + } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct BraintreeRefundTransactionBody { + pub id: String, + pub status: BraintreeRefundStatus, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct BraintreeRefundTransaction { + pub refund: BraintreeRefundTransactionBody, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] +pub struct BraintreeRefundResponseData { + pub refund_transaction: BraintreeRefundTransaction, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] + +pub struct RefundResponse { + pub data: BraintreeRefundResponseData, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: match item.response { + BraintreeRefundResponse::ErrorResponse(error_response) => { + build_error_response(&error_response.errors, item.http_code) + } + BraintreeRefundResponse::SuccessResponse(refund_data) => { + let refund_data = refund_data.data.refund_transaction.refund; + let refund_status = enums::RefundStatus::from(refund_data.status.clone()); + if utils::is_refund_failure(refund_status) { + Err(types::ErrorResponse { + code: refund_data.status.to_string().clone(), + message: refund_data.status.to_string().clone(), + reason: Some(refund_data.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: Some(refund_data.id), + status_code: item.http_code, + }) + } else { + Ok(types::RefundsResponseData { + connector_refund_id: refund_data.id.clone(), + refund_status, + }) + } + } + }, + ..item.data + }) + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct RefundSearchInput { + id: IdFilter, +} +impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundSyncRouterData) -> Result { + let metadata: BraintreeMeta = utils::to_connector_meta_from_secret( + item.connector_meta_data.clone(), + ) + .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata" })?; + utils::validate_currency( + item.request.currency, + Some(metadata.merchant_config_currency), + )?; + let refund_id = item.request.get_connector_refund_id()?; + Ok(Self { + query: REFUND_QUERY.to_string(), + variables: RSyncInput { + input: RefundSearchInput { + id: IdFilter { is: refund_id }, + }, + }, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RSyncNodeData { + id: String, + status: BraintreeRefundStatus, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RSyncEdgeData { + node: RSyncNodeData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RefundData { + edges: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RSyncSearchData { + refunds: RefundData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RSyncResponseData { + search: RSyncSearchData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RSyncResponse { + data: RSyncResponseData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] +pub enum BraintreeRSyncResponse { + RSyncResponse(Box), + ErrorResponse(Box), +} -pub enum ErrorResponse { - BraintreeApiErrorResponse(Box), - BraintreeErrorResponse(Box), +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + match item.response { + BraintreeRSyncResponse::ErrorResponse(error_response) => Ok(Self { + response: build_error_response(&error_response.errors, item.http_code), + ..item.data + }), + BraintreeRSyncResponse::RSyncResponse(rsync_response) => { + let edge_data = rsync_response + .data + .search + .refunds + .edges + .first() + .ok_or(errors::ConnectorError::MissingConnectorRefundID)?; + let connector_refund_id = &edge_data.node.id; + let response = Ok(types::RefundsResponseData { + connector_refund_id: connector_refund_id.to_string(), + refund_status: enums::RefundStatus::from(edge_data.node.status.clone()), + }); + Ok(Self { + response, + ..item.data + }) + } + } + } } -#[derive(Debug, Deserialize, Serialize)] -pub struct ApiErrorResponse { - pub message: String, - pub errors: ErrorsObject, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreditCardData { + number: cards::CardNumber, + expiration_year: Secret, + expiration_month: Secret, + cvv: Secret, + cardholder_name: Secret, } -#[derive(Default, Debug, Clone, Serialize)] -pub struct BraintreeRefundRequest { - transaction: Amount, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientTokenInput { + merchant_account_id: Secret, } -#[derive(Default, Debug, Serialize, Clone)] -pub struct Amount { - amount: Option, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InputData { + credit_card: CreditCardData, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InputClientTokenData { + client_token: ClientTokenInput, } -impl TryFrom<&types::RefundsRouterData> for BraintreeRefundRequest { +impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { - let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?; + fn try_from(item: &types::TokenizationRouterData) -> Result { + match item.request.payment_method_data.clone() { + domain::PaymentMethodData::Card(card_data) => Ok(Self { + query: TOKENIZE_CREDIT_CARD.to_string(), + variables: VariableInput { + input: InputData { + credit_card: CreditCardData { + number: card_data.card_number, + expiration_year: card_data.card_exp_year, + expiration_month: card_data.card_exp_month, + cvv: card_data.card_cvc, + cardholder_name: item + .get_optional_billing_full_name() + .unwrap_or(Secret::new("".to_string())), + }, + }, + }, + }), + domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::Wallet(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankDebit(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::MandatePayment + | domain::PaymentMethodData::OpenBanking(_) + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::RealTimePayment(_) + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("braintree"), + ) + .into()) + } + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenizePaymentMethodData { + id: Secret, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenizeCreditCardData { + payment_method: TokenizePaymentMethodData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientToken { + client_token: Secret, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenizeCreditCard { + tokenize_credit_card: TokenizeCreditCardData, +} - utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?; +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientTokenData { + create_client_token: ClientToken, +} - let refund_amount = - utils::to_currency_base_unit(item.request.refund_amount, item.request.currency)?; +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ClientTokenResponse { + data: ClientTokenData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenResponse { + data: TokenizeCreditCard, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ErrorResponse { + errors: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum BraintreeTokenResponse { + TokenResponse(Box), + ErrorResponse(Box), +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { Ok(Self { - transaction: Amount { - amount: Some(refund_amount), + response: match item.response { + BraintreeTokenResponse::ErrorResponse(error_response) => { + build_error_response(error_response.errors.as_ref(), item.http_code) + } + + BraintreeTokenResponse::TokenResponse(token_response) => { + Ok(types::PaymentsResponseData::TokenizationResponse { + token: token_response + .data + .tokenize_credit_card + .payment_method + .id + .expose() + .clone(), + }) + } }, + ..item.data }) } } -#[allow(dead_code)] -#[derive(Debug, Default, Deserialize, Clone, Serialize)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CaptureTransactionBody { + amount: StringMajorUnit, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CaptureInputData { + transaction_id: String, + transaction: CaptureTransactionBody, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PsyncInputData { + transaction_id: String, +} + +impl TryFrom<&BraintreeRouterData<&types::PaymentsCaptureRouterData>> for BraintreeCaptureRequest { + type Error = error_stack::Report; + fn try_from( + item: &BraintreeRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let query = CAPTURE_TRANSACTION_MUTATION.to_string(); + let variables = VariableCaptureInput { + input: CaptureInputData { + transaction_id: item.router_data.request.connector_transaction_id.clone(), + transaction: CaptureTransactionBody { + amount: item.amount.to_owned(), + }, + }, + }; + Ok(Self { query, variables }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CaptureResponseTransactionBody { + id: String, + status: BraintreePaymentStatus, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CaptureTransactionData { + transaction: CaptureResponseTransactionBody, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CaptureResponseData { + capture_transaction: CaptureTransactionData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CaptureResponse { + data: CaptureResponseData, +} + +impl TryFrom> + for types::PaymentsCaptureRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsCaptureResponseRouterData, + ) -> Result { + match item.response { + BraintreeCaptureResponse::SuccessResponse(capture_data) => { + let transaction_data = capture_data.data.capture_transaction.transaction; + let status = enums::AttemptStatus::from(transaction_data.status.clone()); + let response = if utils::is_payment_failure(status) { + Err(types::ErrorResponse { + code: transaction_data.status.to_string().clone(), + message: transaction_data.status.to_string().clone(), + reason: Some(transaction_data.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: Some(transaction_data.id), + status_code: item.http_code, + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(transaction_data.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } + BraintreeCaptureResponse::ErrorResponse(error_data) => Ok(Self { + response: build_error_response(&error_data.errors, item.http_code), + ..item.data + }), } } } -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct RefundResponse { - pub id: String, - pub status: RefundStatus, +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CancelInputData { + transaction_id: String, } -impl TryFrom> - for types::RefundsRouterData +#[derive(Debug, Serialize)] +pub struct VariableCancelInput { + input: CancelInputData, +} + +#[derive(Debug, Serialize)] +pub struct BraintreeCancelRequest { + query: String, + variables: VariableCancelInput, +} + +impl TryFrom<&types::PaymentsCancelRouterData> for BraintreeCancelRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let query = VOID_TRANSACTION_MUTATION.to_string(); + let variables = VariableCancelInput { + input: CancelInputData { + transaction_id: item.request.connector_transaction_id.clone(), + }, + }; + Ok(Self { query, variables }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CancelResponseTransactionBody { + id: String, + status: BraintreePaymentStatus, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CancelTransactionData { + reversal: CancelResponseTransactionBody, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CancelResponseData { + reverse_transaction: CancelTransactionData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CancelResponse { + data: CancelResponseData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum BraintreeCancelResponse { + CancelResponse(Box), + ErrorResponse(Box), +} + +impl + TryFrom> + for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::ResponseRouterData, ) -> Result { + match item.response { + BraintreeCancelResponse::ErrorResponse(error_response) => Ok(Self { + response: build_error_response(&error_response.errors, item.http_code), + ..item.data + }), + BraintreeCancelResponse::CancelResponse(void_response) => { + let void_data = void_response.data.reverse_transaction.reversal; + let status = enums::AttemptStatus::from(void_data.status.clone()); + let response = if utils::is_payment_failure(status) { + Err(types::ErrorResponse { + code: void_data.status.to_string().clone(), + message: void_data.status.to_string().clone(), + reason: Some(void_data.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } + } + } +} + +impl TryFrom<&types::PaymentsSyncRouterData> for BraintreePSyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsSyncRouterData) -> Result { + let transaction_id = item + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id, - refund_status: enums::RefundStatus::from(item.response.status), + query: TRANSACTION_QUERY.to_string(), + variables: PSyncInput { + input: TransactionSearchInput { + id: IdFilter { is: transaction_id }, + }, + }, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct NodeData { + id: String, + status: BraintreePaymentStatus, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct EdgeData { + node: NodeData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TransactionData { + edges: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SearchData { + transactions: TransactionData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PSyncResponseData { + search: SearchData, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PSyncResponse { + data: PSyncResponseData, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + match item.response { + BraintreePSyncResponse::ErrorResponse(error_response) => Ok(Self { + response: build_error_response(&error_response.errors, item.http_code), + ..item.data }), - ..item.data + BraintreePSyncResponse::SuccessResponse(psync_response) => { + let edge_data = psync_response + .data + .search + .transactions + .edges + .first() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; + let status = enums::AttemptStatus::from(edge_data.node.status.clone()); + let response = if utils::is_payment_failure(status) { + Err(types::ErrorResponse { + code: edge_data.node.status.to_string().clone(), + message: edge_data.node.status.to_string().clone(), + reason: Some(edge_data.node.status.to_string().clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }) + } else { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + edge_data.node.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BraintreeThreeDsResponse { + pub nonce: Secret, + pub liability_shifted: bool, + pub liability_shift_possible: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BraintreeThreeDsErrorResponse { + pub code: String, + pub message: String, +} + +#[derive(Debug, Deserialize)] +pub struct BraintreeRedirectionResponse { + pub authentication_response: String, +} + +impl TryFrom for BraintreeClientTokenRequest { + type Error = error_stack::Report; + fn try_from(metadata: BraintreeMeta) -> Result { + Ok(Self { + query: CLIENT_TOKEN_MUTATION.to_owned(), + variables: VariableClientTokenInput { + input: InputClientTokenData { + client_token: ClientTokenInput { + merchant_account_id: metadata.merchant_account_id, + }, + }, + }, }) } } -impl TryFrom> - for types::RefundsRouterData +impl + TryFrom<( + &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, + BraintreeMeta, + )> for CardPaymentRequest { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + (item, metadata): ( + &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, + BraintreeMeta, + ), ) -> Result { + let (query, transaction_body) = if item.router_data.request.is_mandate_payment() { + ( + match item.router_data.request.is_auto_capture()? { + true => CHARGE_AND_VAULT_TRANSACTION_MUTATION.to_string(), + false => AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION.to_string(), + }, + TransactionBody::Vault(VaultTransactionBody { + amount: item.amount.to_owned(), + merchant_account_id: metadata.merchant_account_id, + vault_payment_method_after_transacting: TransactionTiming { + when: "ALWAYS".to_string(), + }, + }), + ) + } else { + ( + match item.router_data.request.is_auto_capture()? { + true => CHARGE_CREDIT_CARD_MUTATION.to_string(), + false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(), + }, + TransactionBody::Regular(RegularTransactionBody { + amount: item.amount.to_owned(), + merchant_account_id: metadata.merchant_account_id, + channel: CHANNEL_CODE.to_string(), + }), + ) + }; Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id, - refund_status: enums::RefundStatus::from(item.response.status), - }), - ..item.data + query, + variables: VariablePaymentInput { + input: PaymentInput { + payment_method_id: match item.router_data.get_payment_method_token()? { + types::PaymentMethodToken::Token(token) => token, + types::PaymentMethodToken::ApplePayDecrypt(_) => Err( + unimplemented_payment_method!("Apple Pay", "Simplified", "Braintree"), + )?, + }, + transaction: transaction_body, + }, + }, + }) + } +} + +impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for CardPaymentRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let metadata: BraintreeMeta = + utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + utils::validate_currency( + item.router_data.request.currency, + Some(metadata.merchant_config_currency), + )?; + let payload_data = PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload( + &item.router_data.request, + )? + .expose(); + let redirection_response: BraintreeRedirectionResponse = serde_json::from_value( + payload_data, + ) + .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "redirection_response", + })?; + let three_ds_data = serde_json::from_str::( + &redirection_response.authentication_response, + ) + .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "three_ds_data", + })?; + + let (query, transaction_body) = if item.router_data.request.is_mandate_payment() { + ( + match item.router_data.request.is_auto_capture()? { + true => CHARGE_AND_VAULT_TRANSACTION_MUTATION.to_string(), + false => AUTHORIZE_AND_VAULT_CREDIT_CARD_MUTATION.to_string(), + }, + TransactionBody::Vault(VaultTransactionBody { + amount: item.amount.to_owned(), + merchant_account_id: metadata.merchant_account_id, + vault_payment_method_after_transacting: TransactionTiming { + when: "ALWAYS".to_string(), + }, + }), + ) + } else { + ( + match item.router_data.request.is_auto_capture()? { + true => CHARGE_CREDIT_CARD_MUTATION.to_string(), + false => AUTHORIZE_CREDIT_CARD_MUTATION.to_string(), + }, + TransactionBody::Regular(RegularTransactionBody { + amount: item.amount.to_owned(), + merchant_account_id: metadata.merchant_account_id, + channel: CHANNEL_CODE.to_string(), + }), + ) + }; + Ok(Self { + query, + variables: VariablePaymentInput { + input: PaymentInput { + payment_method_id: three_ds_data.nonce, + transaction: transaction_body, + }, + }, }) } } + +fn get_braintree_redirect_form( + client_token_data: ClientTokenResponse, + payment_method_token: types::PaymentMethodToken, + card_details: domain::PaymentMethodData, +) -> Result> { + Ok(services::RedirectForm::Braintree { + client_token: client_token_data + .data + .create_client_token + .client_token + .expose(), + card_token: match payment_method_token { + types::PaymentMethodToken::Token(token) => token.expose(), + types::PaymentMethodToken::ApplePayDecrypt(_) => Err(unimplemented_payment_method!( + "Apple Pay", + "Simplified", + "Braintree" + ))?, + }, + bin: match card_details { + domain::PaymentMethodData::Card(card_details) => { + card_details.card_number.get_card_isin() + } + domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::Wallet(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankDebit(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::MandatePayment + | domain::PaymentMethodData::OpenBanking(_) + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::RealTimePayment(_) + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::CardToken(_) => Err( + errors::ConnectorError::NotImplemented("given payment method".to_owned()), + )?, + }, + }) +} + +#[derive(Debug, Deserialize)] +pub struct BraintreeWebhookResponse { + pub bt_signature: String, + pub bt_payload: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Notification { + pub kind: String, // xml parse only string to fields + pub timestamp: String, + pub dispute: Option, +} +impl types::transformers::ForeignFrom<&str> for api_models::webhooks::IncomingWebhookEvent { + fn foreign_from(status: &str) -> Self { + match status { + "dispute_opened" => Self::DisputeOpened, + "dispute_lost" => Self::DisputeLost, + "dispute_won" => Self::DisputeWon, + "dispute_accepted" | "dispute_auto_accepted" => Self::DisputeAccepted, + "dispute_expired" => Self::DisputeExpired, + "dispute_disputed" => Self::DisputeChallenged, + _ => Self::EventNotSupported, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BraintreeDisputeData { + pub amount_disputed: i64, + pub amount_won: Option, + pub case_number: Option, + pub chargeback_protection_level: Option, + pub currency_iso_code: String, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created_at: Option, + pub evidence: Option, + pub id: String, + pub kind: String, // xml parse only string to fields + pub status: String, + pub reason: Option, + pub reason_code: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub updated_at: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub reply_by_date: Option, + pub transaction: DisputeTransaction, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DisputeTransaction { + pub amount: StringMajorUnit, + pub id: String, +} +#[derive(Debug, Deserialize, Serialize)] +pub struct DisputeEvidence { + pub comment: String, + pub id: Secret, + pub created_at: Option, + pub url: url::Url, +} + +pub(crate) fn get_dispute_stage(code: &str) -> Result { + match code { + "CHARGEBACK" => Ok(enums::DisputeStage::Dispute), + "PRE_ARBITATION" => Ok(enums::DisputeStage::PreArbitration), + "RETRIEVAL" => Ok(enums::DisputeStage::PreDispute), + _ => Err(errors::ConnectorError::WebhookBodyDecodingFailed), + } +} diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 86960b43ca40..c68f0e6069f6 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1730,8 +1730,7 @@ pub enum CybersourceIncrementalAuthorizationStatus { impl ForeignFrom<(CybersourcePaymentStatus, bool)> for enums::AttemptStatus { fn foreign_from((status, capture): (CybersourcePaymentStatus, bool)) -> Self { match status { - CybersourcePaymentStatus::Authorized - | CybersourcePaymentStatus::AuthorizedPendingReview => { + CybersourcePaymentStatus::Authorized => { if capture { // Because Cybersource will return Payment Status as Authorized even in AutoCapture Payment Self::Charged @@ -1739,13 +1738,6 @@ impl ForeignFrom<(CybersourcePaymentStatus, bool)> for enums::AttemptStatus { Self::Authorized } } - CybersourcePaymentStatus::Pending => { - if capture { - Self::Charged - } else { - Self::Pending - } - } CybersourcePaymentStatus::Succeeded | CybersourcePaymentStatus::Transmitted => { Self::Charged } @@ -1762,7 +1754,9 @@ impl ForeignFrom<(CybersourcePaymentStatus, bool)> for enums::AttemptStatus { CybersourcePaymentStatus::PendingReview | CybersourcePaymentStatus::StatusNotReceived | CybersourcePaymentStatus::Challenge - | CybersourcePaymentStatus::Accepted => Self::Pending, + | CybersourcePaymentStatus::Accepted + | CybersourcePaymentStatus::Pending + | CybersourcePaymentStatus::AuthorizedPendingReview => Self::Pending, } } } @@ -1770,8 +1764,8 @@ impl ForeignFrom<(CybersourcePaymentStatus, bool)> for enums::AttemptStatus { impl From for common_enums::AuthorizationStatus { fn from(item: CybersourceIncrementalAuthorizationStatus) -> Self { match item { - CybersourceIncrementalAuthorizationStatus::Authorized - | CybersourceIncrementalAuthorizationStatus::AuthorizedPendingReview => Self::Success, + CybersourceIncrementalAuthorizationStatus::Authorized => Self::Success, + CybersourceIncrementalAuthorizationStatus::AuthorizedPendingReview => Self::Processing, CybersourceIncrementalAuthorizationStatus::Declined => Self::Failure, } } diff --git a/crates/router/src/connector/wellsfargo.rs b/crates/router/src/connector/wellsfargo.rs index 51ff838afd30..f5cdb6431b35 100644 --- a/crates/router/src/connector/wellsfargo.rs +++ b/crates/router/src/connector/wellsfargo.rs @@ -1,13 +1,24 @@ pub mod transformers; -use common_utils::types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}; -use error_stack::{report, ResultExt}; -use masking::ExposeInterface; +use std::fmt::Debug; + +use base64::Engine; +use common_utils::request::RequestContent; +use diesel_models::enums; +use error_stack::{report, Report, ResultExt}; +use masking::{ExposeInterface, PeekInterface}; +use ring::{digest, hmac}; +use time::OffsetDateTime; use transformers as wellsfargo; +use url::Url; -use super::utils::{self as connector_utils}; use crate::{ configs::settings, + connector::{ + utils as connector_utils, + utils::{PaymentMethodDataType, RefundsRequestData}, + }, + consts, core::errors::{self, CustomResult}, events::connector_api_logs::ConnectorEvent, headers, @@ -19,36 +30,313 @@ use crate::{ types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, - ErrorResponse, RequestContent, Response, + transformers::ForeignTryFrom, }, utils::BytesExt, }; -#[derive(Clone)] -pub struct Wellsfargo { - amount_converter: &'static (dyn AmountConvertor + Sync), -} +#[derive(Debug, Clone)] +pub struct Wellsfargo; impl Wellsfargo { - pub fn new() -> &'static Self { - &Self { - amount_converter: &StringMinorUnitForConnector, + pub fn generate_digest(&self, payload: &[u8]) -> String { + let payload_digest = digest::digest(&digest::SHA256, payload); + consts::BASE64_ENGINE.encode(payload_digest) + } + + pub fn generate_signature( + &self, + auth: wellsfargo::WellsfargoAuthType, + host: String, + resource: &str, + payload: &String, + date: OffsetDateTime, + http_method: services::Method, + ) -> CustomResult { + let wellsfargo::WellsfargoAuthType { + api_key, + merchant_account, + api_secret, + } = auth; + let is_post_method = matches!(http_method, services::Method::Post); + let is_patch_method = matches!(http_method, services::Method::Patch); + let is_delete_method = matches!(http_method, services::Method::Delete); + let digest_str = if is_post_method || is_patch_method { + "digest " + } else { + "" + }; + let headers = format!("host date (request-target) {digest_str}v-c-merchant-id"); + let request_target = if is_post_method { + format!("(request-target): post {resource}\ndigest: SHA-256={payload}\n") + } else if is_patch_method { + format!("(request-target): patch {resource}\ndigest: SHA-256={payload}\n") + } else if is_delete_method { + format!("(request-target): delete {resource}\n") + } else { + format!("(request-target): get {resource}\n") + }; + let signature_string = format!( + "host: {host}\ndate: {date}\n{request_target}v-c-merchant-id: {}", + merchant_account.peek() + ); + let key_value = consts::BASE64_ENGINE + .decode(api_secret.expose()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "connector_account_details.api_secret", + })?; + let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value); + let signature_value = + consts::BASE64_ENGINE.encode(hmac::sign(&key, signature_string.as_bytes()).as_ref()); + let signature_header = format!( + r#"keyid="{}", algorithm="HmacSHA256", headers="{headers}", signature="{signature_value}""#, + api_key.peek() + ); + + Ok(signature_header) + } +} + +impl ConnectorCommon for Wellsfargo { + fn id(&self) -> &'static str { + "wellsfargo" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json;charset=utf-8" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.wellsfargo.base_url.as_ref() + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + } + + fn build_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: Result< + wellsfargo::WellsfargoErrorResponse, + Report, + > = res.response.parse_struct("Wellsfargo ErrorResponse"); + + let error_message = if res.status_code == 401 { + consts::CONNECTOR_UNAUTHORIZED_ERROR + } else { + consts::NO_ERROR_MESSAGE + }; + match response { + Ok(transformers::WellsfargoErrorResponse::StandardError(response)) => { + event_builder.map(|i| i.set_error_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + let (code, message, reason) = match response.error_information { + Some(ref error_info) => { + let detailed_error_info = error_info.details.as_ref().map(|details| { + details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", ") + }); + ( + error_info.reason.clone(), + error_info.reason.clone(), + transformers::get_error_reason( + Some(error_info.message.clone()), + detailed_error_info, + None, + ), + ) + } + None => { + let detailed_error_info = response.details.map(|details| { + details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", ") + }); + ( + response + .reason + .clone() + .map_or(consts::NO_ERROR_CODE.to_string(), |reason| { + reason.to_string() + }), + response + .reason + .map_or(error_message.to_string(), |reason| reason.to_string()), + transformers::get_error_reason( + response.message, + detailed_error_info, + None, + ), + ) + } + }; + + Ok(types::ErrorResponse { + status_code: res.status_code, + code, + message, + reason, + attempt_status: None, + connector_transaction_id: None, + }) + } + Ok(transformers::WellsfargoErrorResponse::AuthenticationError(response)) => { + event_builder.map(|i| i.set_error_response_body(&response)); + router_env::logger::info!(connector_response=?response); + Ok(types::ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: response.response.rmsg.clone(), + reason: Some(response.response.rmsg), + attempt_status: None, + connector_transaction_id: None, + }) + } + Ok(transformers::WellsfargoErrorResponse::NotAvailableError(response)) => { + event_builder.map(|i| i.set_error_response_body(&response)); + router_env::logger::info!(connector_response=?response); + let error_response = response + .errors + .iter() + .map(|error_info| { + format!( + "{}: {}", + error_info.error_type.clone().unwrap_or("".to_string()), + error_info.message.clone().unwrap_or("".to_string()) + ) + }) + .collect::>() + .join(" & "); + Ok(types::ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: error_response.clone(), + reason: Some(error_response), + attempt_status: None, + connector_transaction_id: None, + }) + } + Err(error_msg) => { + event_builder.map(|event| event.set_error(serde_json::json!({"error": res.response.escape_ascii().to_string(), "status_code": res.status_code}))); + router_env::logger::error!(deserialization_error =? error_msg); + crate::utils::handle_json_response_deserialization_failure(res, "wellsfargo") + } } } } +impl ConnectorValidation for Wellsfargo { + fn validate_capture_method( + &self, + capture_method: Option, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::ManualMultiple | enums::CaptureMethod::Scheduled => Err( + connector_utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } + fn validate_mandate_payment( + &self, + pm_type: Option, + pm_data: types::domain::payments::PaymentMethodData, + ) -> CustomResult<(), errors::ConnectorError> { + let mandate_supported_pmd = std::collections::HashSet::from([ + PaymentMethodDataType::Card, + PaymentMethodDataType::ApplePay, + PaymentMethodDataType::GooglePay, + ]); + connector_utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id()) + } +} + +impl ConnectorCommonExt for Wellsfargo +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let date = OffsetDateTime::now_utc(); + let wellsfargo_req = self.get_request_body(req, connectors)?; + let auth = wellsfargo::WellsfargoAuthType::try_from(&req.connector_auth_type)?; + let merchant_account = auth.merchant_account.clone(); + let base_url = connectors.wellsfargo.base_url.as_str(); + let wellsfargo_host = + Url::parse(base_url).change_context(errors::ConnectorError::RequestEncodingFailed)?; + let host = wellsfargo_host + .host_str() + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let path: String = self + .get_url(req, connectors)? + .chars() + .skip(base_url.len() - 1) + .collect(); + let sha256 = self.generate_digest(wellsfargo_req.get_inner_value().expose().as_bytes()); + let http_method = self.get_http_method(); + let signature = self.generate_signature( + auth, + host.to_string(), + path.as_str(), + &sha256, + date, + http_method, + )?; + + let mut headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + headers::ACCEPT.to_string(), + "application/hal+json;charset=utf-8".to_string().into(), + ), + ( + "v-c-merchant-id".to_string(), + merchant_account.into_masked(), + ), + ("Date".to_string(), date.to_string().into()), + ("Host".to_string(), host.to_string().into()), + ("Signature".to_string(), signature.into_masked()), + ]; + if matches!( + http_method, + services::Method::Post | services::Method::Put | services::Method::Patch + ) { + headers.push(( + "Digest".to_string(), + format!("SHA-256={sha256}").into_masked(), + )); + } + Ok(headers) + } +} + impl api::Payment for Wellsfargo {} -impl api::PaymentSession for Wellsfargo {} -impl api::ConnectorAccessToken for Wellsfargo {} -impl api::MandateSetup for Wellsfargo {} impl api::PaymentAuthorize for Wellsfargo {} impl api::PaymentSync for Wellsfargo {} -impl api::PaymentCapture for Wellsfargo {} impl api::PaymentVoid for Wellsfargo {} -impl api::Refund for Wellsfargo {} -impl api::RefundExecute for Wellsfargo {} -impl api::RefundSync for Wellsfargo {} +impl api::PaymentCapture for Wellsfargo {} +impl api::PaymentIncrementalAuthorization for Wellsfargo {} +impl api::MandateSetup for Wellsfargo {} +impl api::ConnectorAccessToken for Wellsfargo {} impl api::PaymentToken for Wellsfargo {} +impl api::ConnectorMandateRevoke for Wellsfargo {} impl ConnectorIntegration< @@ -60,111 +348,231 @@ impl // Not Implemented (R) } -impl ConnectorCommonExt for Wellsfargo -where - Self: ConnectorIntegration, +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Wellsfargo { - fn build_headers( + fn get_headers( &self, - req: &types::RouterData, - _connectors: &settings::Connectors, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - self.get_content_type().to_string().into(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + self.build_headers(req, connectors) } -} - -impl ConnectorCommon for Wellsfargo { - fn id(&self) -> &'static str { - "wellsfargo" + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() } - - fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + fn get_url( + &self, + _req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = wellsfargo::WellsfargoZeroMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } - fn common_get_content_type(&self) -> &'static str { - "application/json" + fn build_request( + &self, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .set_body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } - fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { - connectors.wellsfargo.base_url.as_ref() + fn handle_response( + &self, + data: &types::SetupMandateRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult { + let response: wellsfargo::WellsfargoPaymentsResponse = res + .response + .parse_struct("WellsfargoSetupMandatesResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } - fn get_auth_header( + fn get_error_response( &self, - auth_type: &types::ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = wellsfargo::WellsfargoAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) } - fn build_error_response( + fn get_5xx_error_response( &self, - res: Response, + res: types::Response, event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - let response: wellsfargo::WellsfargoErrorResponse = res + ) -> CustomResult { + let response: wellsfargo::WellsfargoServerErrorResponse = res .response - .parse_struct("WellsfargoErrorResponse") + .parse_struct("WellsfargoServerErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); + event_builder.map(|event| event.set_response_body(&response)); + router_env::logger::info!(error_response=?response); - Ok(ErrorResponse { + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(types::ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, - attempt_status: None, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, connector_transaction_id: None, }) } } -impl ConnectorValidation for Wellsfargo { - //TODO: implement functions when support enabled -} - -impl ConnectorIntegration - for Wellsfargo +impl + ConnectorIntegration< + api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > for Wellsfargo { - //TODO: implement sessions flow -} + fn get_headers( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_http_method(&self) -> services::Method { + services::Method::Delete + } + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + fn get_url( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}tms/v1/paymentinstruments/{}", + self.base_url(connectors), + connector_utils::RevokeMandateRequestData::get_connector_mandate_id(&req.request)? + )) + } + fn build_request( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Delete) + .url(&types::MandateRevokeType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::MandateRevokeType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::MandateRevokeRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult { + if matches!(res.status_code, 204) { + event_builder.map(|i| i.set_response_body(&serde_json::json!({"mandate_status": common_enums::MandateStatus::Revoked.to_string()}))); + Ok(types::MandateRevokeRouterData { + response: Ok(types::MandateRevokeResponseData { + mandate_status: common_enums::MandateStatus::Revoked, + }), + ..data.clone() + }) + } else { + // If http_code != 204 || http_code != 4xx, we dont know any other response scenario yet. + let response_value: serde_json::Value = serde_json::from_slice(&res.response) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + let response_string = response_value.to_string(); + event_builder.map(|i| { + i.set_response_body( + &serde_json::json!({"response_string": response_string.clone()}), + ) + }); + router_env::logger::info!(connector_response=?response_string); + + Ok(types::MandateRevokeRouterData { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: response_string.clone(), + reason: Some(response_string), + status_code: res.status_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..data.clone() + }) + } + } + fn get_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} impl ConnectorIntegration for Wellsfargo { + // Not Implemented (R) } -impl - ConnectorIntegration< - api::SetupMandate, - types::SetupMandateRequestData, - types::PaymentsResponseData, - > for Wellsfargo +impl api::PaymentSession for Wellsfargo {} + +impl ConnectorIntegration + for Wellsfargo { } -impl ConnectorIntegration +impl ConnectorIntegration for Wellsfargo { fn get_headers( &self, - req: &types::PaymentsAuthorizeRouterData, + req: &types::PaymentsCaptureRouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -176,60 +584,63 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{}/captures", + self.base_url(connectors), + connector_payment_id + )) } fn get_request_body( &self, - req: &types::PaymentsAuthorizeRouterData, + req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let amount = connector_utils::convert_amount( - self.amount_converter, - req.request.minor_amount, + let connector_router_data = wellsfargo::WellsfargoRouterData::try_from(( + &self.get_currency_unit(), req.request.currency, - )?; - - let connector_router_data = wellsfargo::WellsfargoRouterData::from((amount, req)); + req.request.amount_to_capture, + req, + ))?; let connector_req = - wellsfargo::WellsfargoPaymentsRequest::try_from(&connector_router_data)?; + wellsfargo::WellsfargoPaymentsCaptureRequest::try_from(&connector_router_data)?; Ok(RequestContent::Json(Box::new(connector_req))) } - fn build_request( &self, - req: &types::PaymentsAuthorizeRouterData, + req: &types::PaymentsCaptureRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::PaymentsAuthorizeType::get_url( - self, req, connectors, - )?) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsAuthorizeType::get_headers( + .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .set_body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsCaptureType::get_request_body( self, req, connectors, )?) .build(), )) } - fn handle_response( &self, - data: &types::PaymentsAuthorizeRouterData, + data: &types::PaymentsCaptureRouterData, event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { + res: types::Response, + ) -> CustomResult< + types::RouterData, + errors::ConnectorError, + > { let response: wellsfargo::WellsfargoPaymentsResponse = res .response - .parse_struct("Wellsfargo PaymentsAuthorizeResponse") + .parse_struct("Wellsfargo PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -239,14 +650,38 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult { self.build_error_response(res, event_builder) } + + fn get_5xx_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: wellsfargo::WellsfargoServerErrorResponse = res + .response + .parse_struct("WellsfargoServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|event| event.set_response_body(&response)); + router_env::logger::info!(error_response=?response); + + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -260,16 +695,29 @@ impl ConnectorIntegration &'static str { - self.common_get_content_type() + fn get_http_method(&self) -> services::Method { + services::Method::Get } fn get_url( &self, - _req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}tss/v2/transactions/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() } fn build_request( @@ -286,16 +734,15 @@ impl ConnectorIntegration, - res: Response, + res: types::Response, ) -> CustomResult { - let response: wellsfargo::WellsfargoPaymentsResponse = res + let response: wellsfargo::WellsfargoTransactionResponse = res .response - .parse_struct("wellsfargo PaymentsSyncResponse") + .parse_struct("Wellsfargo PaymentSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -305,22 +752,21 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult { self.build_error_response(res, event_builder) } } -impl ConnectorIntegration +impl ConnectorIntegration for Wellsfargo { fn get_headers( &self, - req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -332,49 +778,187 @@ impl ConnectorIntegration CustomResult { + Ok(format!( + "{}pts/v2/payments/", + ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = wellsfargo::WellsfargoRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = + wellsfargo::WellsfargoPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(self.get_request_body(req, connectors)?) + .build(); + + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult { + let response: wellsfargo::WellsfargoPaymentsResponse = res + .response + .parse_struct("Wellsfargo PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } + + fn get_5xx_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: wellsfargo::WellsfargoServerErrorResponse = res + .response + .parse_struct("WellsfargoServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|event| event.set_response_body(&response)); + router_env::logger::info!(error_response=?response); + + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } +} + +impl ConnectorIntegration + for Wellsfargo +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{connector_payment_id}/reversals", + self.base_url(connectors) + )) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let connector_router_data = wellsfargo::WellsfargoRouterData::try_from(( + &self.get_currency_unit(), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + })?, + req.request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Amount", + })?, + req, + ))?; + let connector_req = wellsfargo::WellsfargoVoidRequest::try_from(&connector_router_data)?; + + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, - req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCancelRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( - self, req, connectors, - )?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(self.get_request_body(req, connectors)?) .build(), )) } fn handle_response( &self, - data: &types::PaymentsCaptureRouterData, + data: &types::PaymentsCancelRouterData, event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { + res: types::Response, + ) -> CustomResult { let response: wellsfargo::WellsfargoPaymentsResponse = res .response - .parse_struct("Wellsfargo PaymentsCaptureResponse") + .parse_struct("Wellsfargo PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -387,24 +971,48 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult { self.build_error_response(res, event_builder) } -} -impl ConnectorIntegration - for Wellsfargo -{ + fn get_5xx_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: wellsfargo::WellsfargoServerErrorResponse = res + .response + .parse_struct("WellsfargoServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|event| event.set_response_body(&response)); + router_env::logger::info!(error_response=?response); + + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } +impl api::Refund for Wellsfargo {} +impl api::RefundExecute for Wellsfargo {} +impl api::RefundSync for Wellsfargo {} + impl ConnectorIntegration for Wellsfargo { fn get_headers( &self, - req: &types::RefundsRouterData, + req: &types::RefundExecuteRouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -416,56 +1024,58 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + req: &types::RefundExecuteRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{}/refunds", + self.base_url(connectors), + connector_payment_id + )) } fn get_request_body( &self, - req: &types::RefundsRouterData, + req: &types::RefundExecuteRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let refund_amount = connector_utils::convert_amount( - self.amount_converter, - req.request.minor_refund_amount, + let connector_router_data = wellsfargo::WellsfargoRouterData::try_from(( + &self.get_currency_unit(), req.request.currency, - )?; - - let connector_router_data = wellsfargo::WellsfargoRouterData::from((refund_amount, req)); + req.request.refund_amount, + req, + ))?; let connector_req = wellsfargo::WellsfargoRefundRequest::try_from(&connector_router_data)?; Ok(RequestContent::Json(Box::new(connector_req))) } - fn build_request( &self, - req: &types::RefundsRouterData, + req: &types::RefundExecuteRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let request = services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundExecuteType::get_headers( - self, req, connectors, - )?) - .set_body(types::RefundExecuteType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(self.get_request_body(req, connectors)?) + .build(), + )) } fn handle_response( &self, - data: &types::RefundsRouterData, + data: &types::RefundExecuteRouterData, event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult, errors::ConnectorError> { - let response: wellsfargo::RefundResponse = res + res: types::Response, + ) -> CustomResult { + let response: wellsfargo::WellsfargoRefundResponse = res .response - .parse_struct("wellsfargo RefundResponse") + .parse_struct("Wellsfargo RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -475,12 +1085,11 @@ impl ConnectorIntegration, - ) -> CustomResult { + ) -> CustomResult { self.build_error_response(res, event_builder) } } @@ -495,19 +1104,24 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } - fn get_content_type(&self) -> &'static str { self.common_get_content_type() } - + fn get_http_method(&self) -> services::Method { + services::Method::Get + } fn get_url( &self, - _req: &types::RefundSyncRouterData, - _connectors: &settings::Connectors, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let refund_id = req.request.get_connector_refund_id()?; + Ok(format!( + "{}tss/v2/transactions/{}", + self.base_url(connectors), + refund_id + )) } - fn build_request( &self, req: &types::RefundSyncRouterData, @@ -519,22 +1133,18 @@ impl ConnectorIntegration, - res: Response, + res: types::Response, ) -> CustomResult { - let response: wellsfargo::RefundResponse = res + let response: wellsfargo::WellsfargoRsyncResponse = res .response - .parse_struct("wellsfargo RefundSyncResponse") + .parse_struct("Wellsfargo RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -544,12 +1154,123 @@ impl ConnectorIntegration, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl + ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for Wellsfargo +{ + fn get_headers( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_http_method(&self) -> services::Method { + services::Method::Patch + } + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = wellsfargo::WellsfargoRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.additional_amount, + req, + ))?; + let connector_request = + wellsfargo::WellsfargoPaymentsIncrementalAuthorizationRequest::try_from( + &connector_router_data, + )?; + Ok(RequestContent::Json(Box::new(connector_request))) + } + fn build_request( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Patch) + .url(&types::IncrementalAuthorizationType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::IncrementalAuthorizationType::get_headers( + self, req, connectors, + )?) + .set_body(types::IncrementalAuthorizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::PaymentsIncrementalAuthorizationRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult< + types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > { + let response: wellsfargo::WellsfargoPaymentsIncrementalAuthorizationResponse = res + .response + .parse_struct("Wellsfargo PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::foreign_try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + true, + )) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } fn get_error_response( &self, - res: Response, + res: types::Response, event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { + ) -> CustomResult { self.build_error_response(res, event_builder) } } @@ -559,7 +1280,7 @@ impl api::IncomingWebhook for Wellsfargo { fn get_webhook_object_reference_id( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult { Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } @@ -567,7 +1288,7 @@ impl api::IncomingWebhook for Wellsfargo { &self, _request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + Ok(api::IncomingWebhookEvent::EventNotSupported) } fn get_webhook_resource_object( diff --git a/crates/router/src/connector/wellsfargo/transformers.rs b/crates/router/src/connector/wellsfargo/transformers.rs index d0a1bb414021..93ed04288afe 100644 --- a/crates/router/src/connector/wellsfargo/transformers.rs +++ b/crates/router/src/connector/wellsfargo/transformers.rs @@ -1,43 +1,1133 @@ -use common_utils::types::StringMinorUnit; -use masking::Secret; +use api_models::payments; +use base64::Engine; +use common_enums::FutureUsage; +use common_utils::{pii, types::SemanticVersion}; +use masking::{ExposeInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{ + self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData, + PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RecurringMandateData, RouterData, + }, + consts, core::errors, - types::{self, api, domain, storage::enums}, + types::{ + self, + api::{self, enums as api_enums}, + domain, + storage::enums, + transformers::{ForeignFrom, ForeignTryFrom}, + ApplePayPredecryptData, + }, + unimplemented_payment_method, }; -//TODO: Fill the struct with respective fields +#[derive(Debug, Serialize)] pub struct WellsfargoRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: String, pub router_data: T, } -impl From<(StringMinorUnit, T)> for WellsfargoRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts - Self { +impl TryFrom<(&api::CurrencyUnit, enums::Currency, i64, T)> for WellsfargoRouterData { + type Error = error_stack::Report; + fn try_from( + (currency_unit, currency, amount, item): (&api::CurrencyUnit, enums::Currency, i64, T), + ) -> Result { + // This conversion function is used at different places in the file, if updating this, keep a check for those + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { amount, router_data: item, - } + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoZeroMandateRequest { + processing_information: ProcessingInformation, + payment_information: PaymentInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&types::SetupMandateRouterData> for WellsfargoZeroMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::SetupMandateRouterData) -> Result { + let email = item.request.get_email()?; + let bill_to = build_bill_to(item.get_optional_billing(), email)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: "0".to_string(), + currency: item.request.currency, + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = ( + Some(vec![WellsfargoActionsList::TokenCreate]), + Some(vec![ + WellsfargoActionsTokenType::PaymentInstrument, + WellsfargoActionsTokenType::Customer, + ]), + Some(WellsfargoAuthorizationOptions { + initiator: Some(WellsfargoPaymentInitiator { + initiator_type: Some(WellsfargoPaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, + }), + merchant_intitiated_transaction: None, + }), + ); + + let client_reference_information = ClientReferenceInformation { + code: Some(item.connector_request_reference_id.clone()), + }; + + let (payment_information, solution) = match item.request.payment_method_data.clone() { + domain::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + ( + PaymentInformation::Cards(Box::new(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: Some(ccard.card_cvc), + card_type, + }, + })), + None, + ) + } + + domain::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + domain::WalletData::ApplePay(apple_pay_data) => { + match item.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + let expiration_month = decrypt_data.get_expiry_month()?; + let expiration_year = decrypt_data.get_four_digit_expiry_year()?; + ( + PaymentInformation::ApplePay(Box::new( + ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: decrypt_data + .application_primary_account_number, + cryptogram: decrypt_data + .payment_data + .online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, + }, + }, + )), + Some(PaymentSolution::ApplePay), + ) + } + types::PaymentMethodToken::Token(_) => Err( + unimplemented_payment_method!("Apple Pay", "Manual", "Wellsfargo"), + )?, + }, + None => ( + PaymentInformation::ApplePayToken(Box::new( + ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + descriptor: Some(FLUID_DATA_DESCRIPTOR.to_string()), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }, + )), + Some(PaymentSolution::ApplePay), + ), + } + } + domain::WalletData::GooglePay(google_pay_data) => ( + PaymentInformation::GooglePay(Box::new(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE + .encode(google_pay_data.tokenization_data.token), + ), + descriptor: None, + }, + })), + Some(PaymentSolution::GooglePay), + ), + domain::WalletData::AliPayQr(_) + | domain::WalletData::AliPayRedirect(_) + | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::MomoRedirect(_) + | domain::WalletData::KakaoPayRedirect(_) + | domain::WalletData::GoPayRedirect(_) + | domain::WalletData::GcashRedirect(_) + | domain::WalletData::ApplePayRedirect(_) + | domain::WalletData::ApplePayThirdPartySdk(_) + | domain::WalletData::DanaRedirect {} + | domain::WalletData::GooglePayRedirect(_) + | domain::WalletData::GooglePayThirdPartySdk(_) + | domain::WalletData::MbWayRedirect(_) + | domain::WalletData::MobilePayRedirect(_) + | domain::WalletData::PaypalRedirect(_) + | domain::WalletData::PaypalSdk(_) + | domain::WalletData::SamsungPay(_) + | domain::WalletData::TwintRedirect {} + | domain::WalletData::VippsRedirect {} + | domain::WalletData::TouchNGoRedirect(_) + | domain::WalletData::WeChatPayRedirect(_) + | domain::WalletData::WeChatPayQr(_) + | domain::WalletData::CashappQr(_) + | domain::WalletData::SwishQr(_) + | domain::WalletData::Mifinity(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wellsfargo"), + ))?, + }, + domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankDebit(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::MandatePayment + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::RealTimePayment(_) + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::OpenBanking(_) + | domain::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wellsfargo"), + ))? + } + }; + + let processing_information = ProcessingInformation { + capture: Some(false), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: String::from("internet"), + payment_solution: solution.map(String::from), + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct WellsfargoPaymentsRequest { - amount: StringMinorUnit, - card: WellsfargoCard, + processing_information: ProcessingInformation, + payment_information: PaymentInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + consumer_authentication_information: Option, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProcessingInformation { + action_list: Option>, + action_token_types: Option>, + authorization_options: Option, + commerce_indicator: String, + capture: Option, + capture_options: Option, + payment_solution: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoConsumerAuthInformation { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option>, + xid: Option, + directory_server_transaction_id: Option>, + specification_version: Option, + /// This field specifies the 3ds version + pa_specification_version: Option, + /// Verification response enrollment status. + /// + /// This field is supported only on Asia, Middle East, and Africa Gateway. + /// + /// For external authentication, this field will always be "Y" + veres_enrolled: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantDefinedInformation { + key: u8, + value: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum WellsfargoActionsList { + TokenCreate, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum WellsfargoActionsTokenType { + Customer, + PaymentInstrument, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoAuthorizationOptions { + initiator: Option, + merchant_intitiated_transaction: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantInitiatedTransaction { + reason: Option, + previous_transaction_id: Option>, + //Required for recurring mandates payment + original_authorized_amount: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoPaymentInitiator { + #[serde(rename = "type")] + initiator_type: Option, + credential_stored_on_file: Option, + stored_credential_used: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum WellsfargoPaymentInitiatorTypes { + Customer, + Merchant, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CaptureOptions { + capture_sequence_number: u32, + total_capture_count: u32, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardPaymentInformation { + card: Card, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenizedCard { + number: Secret, + expiration_month: Secret, + expiration_year: Secret, + cryptogram: Secret, + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenizedCard { + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenPaymentInformation { + fluid_data: FluidData, + tokenized_card: ApplePayTokenizedCard, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayPaymentInformation { + tokenized_card: TokenizedCard, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MandatePaymentInformation { + payment_instrument: WellsfargoPaymentInstrument, +} +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct AchBankAccount { + account: Account, + routing_number: Secret, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Account { + #[serde(rename = "type")] + account_type: AccountType, + number: Secret, +} +#[derive(Debug, Deserialize, Serialize)] +enum AccountType { + /// Checking account type. + C, + /// General ledger account type. Supported only on Wells Fargo ACH. + G, + /// Savings account type. + S, + /// Corporate checking account type. + X, +} + +#[derive(Debug, Serialize)] +pub struct AchPaymentInformation { + bank: AchBankAccount, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FluidData { + value: Secret, + #[serde(skip_serializing_if = "Option::is_none")] + descriptor: Option, +} + +pub const FLUID_DATA_DESCRIPTOR: &str = "RklEPUNPTU1PTi5BUFBMRS5JTkFQUC5QQVlNRU5U"; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayPaymentInformation { + fluid_data: FluidData, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct WellsfargoCard { +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum PaymentInformation { + Cards(Box), + GooglePay(Box), + ApplePay(Box), + ApplePayToken(Box), + MandatePayment(Box), + AchDebitPayment(Box), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WellsfargoPaymentInstrument { + id: Secret, +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Card { number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + expiration_month: Secret, + expiration_year: Secret, + security_code: Option>, + #[serde(rename = "type")] + card_type: Option, +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationWithBill { + amount_details: Amount, + bill_to: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformation { + amount_details: Amount, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Amount { + total_amount: String, + currency: api_models::enums::Currency, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalAmount { + additional_amount: String, + currency: String, +} + +#[derive(Debug, Serialize)] +pub enum PaymentSolution { + ApplePay, + GooglePay, +} + +#[derive(Debug, Serialize)] +pub enum TransactionType { + #[serde(rename = "1")] + ApplePay, +} + +impl From for String { + fn from(solution: PaymentSolution) -> Self { + let payment_solution = match solution { + PaymentSolution::ApplePay => "001", + PaymentSolution::GooglePay => "012", + }; + payment_solution.to_string() + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BillTo { + first_name: Option>, + last_name: Option>, + address1: Option>, + locality: Option, + #[serde(skip_serializing_if = "Option::is_none")] + administrative_area: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + postal_code: Option>, + country: Option, + email: pii::Email, + phone_number: Option>, +} + +impl From<&WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + +impl + TryFrom<( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + Option, + )> for ProcessingInformation +{ + type Error = error_stack::Report; + fn try_from( + (item, solution, network): ( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + Option, + ), + ) -> Result { + let mut commerce_indicator = solution + .as_ref() + .map(|pm_solution| match pm_solution { + PaymentSolution::ApplePay => network + .as_ref() + .map(|card_network| match card_network.to_lowercase().as_str() { + "amex" => "aesk", + "discover" => "dipb", + "mastercard" => "spa", + "visa" => "internet", + _ => "internet", + }) + .unwrap_or("internet"), + PaymentSolution::GooglePay => "internet", + }) + .unwrap_or("internet") + .to_string(); + + let (action_list, action_token_types, authorization_options) = if item + .router_data + .request + .setup_future_usage + .map_or(false, |future_usage| { + matches!(future_usage, FutureUsage::OffSession) + }) + && (item.router_data.request.customer_acceptance.is_some() + || item + .router_data + .request + .setup_mandate_details + .clone() + .map_or(false, |mandate_details| { + mandate_details.customer_acceptance.is_some() + })) { + ( + Some(vec![WellsfargoActionsList::TokenCreate]), + Some(vec![ + WellsfargoActionsTokenType::PaymentInstrument, + WellsfargoActionsTokenType::Customer, + ]), + Some(WellsfargoAuthorizationOptions { + initiator: Some(WellsfargoPaymentInitiator { + initiator_type: Some(WellsfargoPaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, + }), + merchant_intitiated_transaction: None, + }), + ) + } else if item.router_data.request.mandate_id.is_some() { + match item + .router_data + .request + .mandate_id + .clone() + .and_then(|mandate_id| mandate_id.mandate_reference_id) + { + Some(payments::MandateReferenceId::ConnectorMandateId(_)) => { + let original_amount = item + .router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_amount()?; + let original_currency = item + .router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_currency()?; + ( + None, + None, + Some(WellsfargoAuthorizationOptions { + initiator: None, + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: None, + original_authorized_amount: Some(utils::get_amount_as_string( + &api::CurrencyUnit::Base, + original_amount, + original_currency, + )?), + previous_transaction_id: None, + }), + }), + ) + } + Some(payments::MandateReferenceId::NetworkMandateId(network_transaction_id)) => { + let (original_amount, original_currency) = match network + .clone() + .map(|network| network.to_lowercase()) + .as_deref() + { + Some("discover") => { + 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, original_currency) { + (Some(original_amount), Some(original_currency)) => Some( + utils::to_currency_base_unit(original_amount, original_currency)?, + ), + _ => None, + }; + commerce_indicator = "recurring".to_string(); + ( + None, + None, + Some(WellsfargoAuthorizationOptions { + initiator: Some(WellsfargoPaymentInitiator { + initiator_type: Some(WellsfargoPaymentInitiatorTypes::Merchant), + credential_stored_on_file: None, + stored_credential_used: Some(true), + }), + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: Some("7".to_string()), + original_authorized_amount, + previous_transaction_id: Some(Secret::new(network_transaction_id)), + }), + }), + ) + } + None => (None, None, None), + } + } else { + (None, None, None) + }; + // this logic is for external authenticated card + let commerce_indicator_for_external_authentication = item + .router_data + .request + .authentication_data + .as_ref() + .and_then(|authn_data| { + authn_data + .eci + .clone() + .map(|eci| get_commerce_indicator_for_external_authentication(network, eci)) + }); + + Ok(Self { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + action_list, + action_token_types, + authorization_options, + capture_options: None, + commerce_indicator: commerce_indicator_for_external_authentication + .unwrap_or(commerce_indicator), + }) + } +} + +fn get_commerce_indicator_for_external_authentication( + card_network: Option, + eci: String, +) -> String { + let card_network_lower_case = card_network + .as_ref() + .map(|card_network| card_network.to_lowercase()); + match eci.as_str() { + "00" | "01" | "02" => { + if matches!( + card_network_lower_case.as_deref(), + Some("mastercard") | Some("maestro") + ) { + "spa" + } else { + "internet" + } + } + "05" => match card_network_lower_case.as_deref() { + Some("amex") => "aesk", + Some("discover") => "dipb", + Some("mastercard") => "spa", + Some("visa") => "vbv", + Some("diners") => "pb", + Some("upi") => "up3ds", + _ => "internet", + }, + "06" => match card_network_lower_case.as_deref() { + Some("amex") => "aesk_attempted", + Some("discover") => "dipb_attempted", + Some("mastercard") => "spa", + Some("visa") => "vbv_attempted", + Some("diners") => "pb_attempted", + Some("upi") => "up3ds_attempted", + _ => "internet", + }, + "07" => match card_network_lower_case.as_deref() { + Some("amex") => "internet", + Some("discover") => "internet", + Some("mastercard") => "spa", + Some("visa") => "vbv_failure", + Some("diners") => "internet", + Some("upi") => "up3ds_failure", + _ => "internet", + }, + _ => "vbv_failure", + } + .to_string() +} + +impl + From<( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to, + } + } +} + +fn get_phone_number(item: Option<&payments::Address>) -> Option> { + item.as_ref() + .and_then(|billing| billing.phone.as_ref()) + .and_then(|phone| { + phone.number.as_ref().and_then(|number| { + phone + .country_code + .as_ref() + .map(|cc| Secret::new(format!("{}{}", cc, number.peek()))) + }) + }) +} + +fn build_bill_to( + address_details: Option<&payments::Address>, + email: pii::Email, +) -> Result> { + let phone_number = get_phone_number(address_details); + println!(" ADESSS {:?}", address_details.clone()); + let default_address = BillTo { + first_name: None, + last_name: None, + address1: None, + locality: None, + administrative_area: None, + postal_code: None, + country: None, + email: email.clone(), + phone_number: phone_number.clone(), + }; + let ad = Ok(address_details + .and_then(|addr| { + addr.address.as_ref().map(|addr| BillTo { + first_name: addr.first_name.clone(), + last_name: addr.last_name.clone(), + address1: addr.line1.clone(), + locality: addr.city.clone(), + administrative_area: addr.to_state_code_as_optional().ok().flatten(), + postal_code: addr.zip.clone(), + country: addr.country, + email, + phone_number: phone_number.clone(), + }) + }) + .unwrap_or(default_address)); + ad +} + +impl ForeignFrom for Vec { + fn foreign_from(metadata: Value) -> Self { + let hashmap: std::collections::BTreeMap = + serde_json::from_str(&metadata.to_string()) + .unwrap_or(std::collections::BTreeMap::new()); + let mut vector: Self = Self::new(); + let mut iter = 1; + for (key, value) in hashmap { + vector.push(MerchantDefinedInformation { + key: iter, + value: format!("{key}={value}"), + }); + iter += 1; + } + vector + } +} + +impl + TryFrom<( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + domain::Card, + )> for WellsfargoPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + domain::Card, + ), + ) -> 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 = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + + let payment_information = PaymentInformation::Cards(Box::new(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: Some(ccard.card_cvc), + card_type: card_type.clone(), + }, + })); + + 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 ccard.card_network == Some(common_enums::CardNetwork::Mastercard) { + (Some(Secret::new(authn_data.cavv.clone())), None) + } else { + (None, Some(authn_data.cavv.clone())) + }; + WellsfargoConsumerAuthInformation { + 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<( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + domain::ApplePayWalletData, + )> for WellsfargoPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, apple_pay_data, apple_pay_wallet_data): ( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + domain::ApplePayWalletData, + ), + ) -> 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 processing_information = ProcessingInformation::try_from(( + item, + Some(PaymentSolution::ApplePay), + Some(apple_pay_wallet_data.payment_method.network.clone()), + ))?; + let client_reference_information = ClientReferenceInformation::from(item); + let expiration_month = apple_pay_data.get_expiry_month()?; + let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; + let payment_information = + PaymentInformation::ApplePay(Box::new(ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: apple_pay_data.application_primary_account_number, + cryptogram: apple_pay_data.payment_data.online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, + }, + })); + let merchant_defined_information = item + .router_data + .request + .metadata + .clone() + .map(Vec::::foreign_from); + let ucaf_collection_indicator = match apple_pay_wallet_data + .payment_method + .network + .to_lowercase() + .as_str() + { + "mastercard" => Some("2".to_string()), + _ => None, + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information: Some(WellsfargoConsumerAuthInformation { + ucaf_collection_indicator, + cavv: None, + ucaf_authentication_data: None, + xid: None, + directory_server_transaction_id: None, + specification_version: None, + pa_specification_version: None, + veres_enrolled: None, + }), + merchant_defined_information, + }) + } +} + +impl + TryFrom<( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + domain::GooglePayWalletData, + )> for WellsfargoPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, google_pay_data): ( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + domain::GooglePayWalletData, + ), + ) -> 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 payment_information = + PaymentInformation::GooglePay(Box::new(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE.encode(google_pay_data.tokenization_data.token), + ), + descriptor: None, + }, + })); + let processing_information = + ProcessingInformation::try_from((item, Some(PaymentSolution::GooglePay), None))?; + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = item + .router_data + .request + .metadata + .clone() + .map(Vec::::foreign_from); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information: None, + merchant_defined_information, + }) + } +} + +impl TryFrom> for AccountType { + type Error = error_stack::Report; + + fn try_from(optional_bank_type: Option) -> Result { + match optional_bank_type { + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "bank_type", + })?, + Some(bank_type) => match bank_type { + common_enums::BankType::Checking => Ok(Self::C), + common_enums::BankType::Savings => Ok(Self::S), + }, + } + } +} + +impl + TryFrom<( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + domain::BankDebitData, + )> for WellsfargoPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, bank_debit_data): ( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + domain::BankDebitData, + ), + ) -> 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 payment_information = match bank_debit_data { + domain::BankDebitData::AchBankDebit { + account_number, + routing_number, + bank_type, + .. + } => Ok(PaymentInformation::AchDebitPayment(Box::new( + AchPaymentInformation { + bank: AchBankAccount { + account: Account { + account_type: AccountType::try_from(bank_type)?, + number: account_number, + }, + routing_number, + }, + }, + ))), + domain::BankDebitData::SepaBankDebit { .. } + | domain::BankDebitData::BacsBankDebit { .. } + | domain::BankDebitData::BecsBankDebit { .. } => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wellsfargo"), + )) + } + }?; + let processing_information = + ProcessingInformation::try_from((item, Some(PaymentSolution::GooglePay), None))?; + let client_reference_information = ClientReferenceInformation::from(item); + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information: None, + merchant_defined_information: None, + }) + } } impl TryFrom<&WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>> @@ -47,107 +1137,1038 @@ impl TryFrom<&WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { - domain::PaymentMethodData::Card(req_card) => { - let card = WellsfargoCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.amount.clone(), - card, - }) + match item.router_data.request.connector_mandate_id() { + Some(connector_mandate_id) => Self::try_from((item, connector_mandate_id)), + None => { + match item.router_data.request.payment_method_data.clone() { + domain::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + domain::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + domain::WalletData::ApplePay(apple_pay_data) => { + match item.router_data.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data, apple_pay_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(unimplemented_payment_method!( + "Apple Pay", + "Manual", + "Wellsfargo" + ))? + } + }, + None => { + 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 processing_information = + ProcessingInformation::try_from(( + item, + Some(PaymentSolution::ApplePay), + Some(apple_pay_data.payment_method.network.clone()), + ))?; + let client_reference_information = + ClientReferenceInformation::from(item); + let payment_information = PaymentInformation::ApplePayToken( + Box::new(ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + descriptor: Some(FLUID_DATA_DESCRIPTOR.to_string()), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }), + ); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from( + metadata, + ) + }); + let ucaf_collection_indicator = match apple_pay_data + .payment_method + .network + .to_lowercase() + .as_str() + { + "mastercard" => Some("2".to_string()), + _ => None, + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: Some( + WellsfargoConsumerAuthInformation { + ucaf_collection_indicator, + cavv: None, + ucaf_authentication_data: None, + xid: None, + directory_server_transaction_id: None, + specification_version: None, + pa_specification_version: None, + veres_enrolled: None, + }, + ), + }) + } + } + } + domain::WalletData::GooglePay(google_pay_data) => { + Self::try_from((item, google_pay_data)) + } + domain::WalletData::AliPayQr(_) + | domain::WalletData::AliPayRedirect(_) + | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::MomoRedirect(_) + | domain::WalletData::KakaoPayRedirect(_) + | domain::WalletData::GoPayRedirect(_) + | domain::WalletData::GcashRedirect(_) + | domain::WalletData::ApplePayRedirect(_) + | domain::WalletData::ApplePayThirdPartySdk(_) + | domain::WalletData::DanaRedirect {} + | domain::WalletData::GooglePayRedirect(_) + | domain::WalletData::GooglePayThirdPartySdk(_) + | domain::WalletData::MbWayRedirect(_) + | domain::WalletData::MobilePayRedirect(_) + | domain::WalletData::PaypalRedirect(_) + | domain::WalletData::PaypalSdk(_) + | domain::WalletData::SamsungPay(_) + | domain::WalletData::TwintRedirect {} + | domain::WalletData::VippsRedirect {} + | domain::WalletData::TouchNGoRedirect(_) + | domain::WalletData::WeChatPayRedirect(_) + | domain::WalletData::WeChatPayQr(_) + | domain::WalletData::CashappQr(_) + | domain::WalletData::SwishQr(_) + | domain::WalletData::Mifinity(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wellsfargo"), + ) + .into()) + } + }, + // If connector_mandate_id is present MandatePayment will be the PMD, the case will be handled in the first `if` clause. + // This is a fallback implementation in the event of catastrophe. + domain::PaymentMethodData::MandatePayment => { + let connector_mandate_id = + item.router_data.request.connector_mandate_id().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_id", + }, + )?; + Self::try_from((item, connector_mandate_id)) + } + domain::PaymentMethodData::BankDebit(bank_debit) => { + Self::try_from((item, bank_debit)) + } + domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::RealTimePayment(_) + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::OpenBanking(_) + | domain::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wellsfargo"), + ) + .into()) + } + } } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), } } } -//TODO: Fill the struct with respective fields -// Auth Struct +impl + TryFrom<( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + String, + )> for WellsfargoPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, connector_mandate_id): ( + &WellsfargoRouterData<&types::PaymentsAuthorizeRouterData>, + String, + ), + ) -> Result { + let processing_information = ProcessingInformation::try_from((item, None, None))?; + let payment_instrument = WellsfargoPaymentInstrument { + id: connector_mandate_id.into(), + }; + let bill_to = + item.router_data.request.get_email().ok().and_then(|email| { + build_bill_to(item.router_data.get_optional_billing(), email).ok() + }); + let order_information = OrderInformationWithBill::from((item, bill_to)); + let payment_information = + PaymentInformation::MandatePayment(Box::new(MandatePaymentInformation { + payment_instrument, + })); + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = item + .router_data + .request + .metadata + .clone() + .map(Vec::::foreign_from); + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoPaymentsCaptureRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoPaymentsIncrementalAuthorizationRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationIncrementalAuthorization, +} + +impl TryFrom<&WellsfargoRouterData<&types::PaymentsCaptureRouterData>> + for WellsfargoPaymentsCaptureRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &WellsfargoRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { + let merchant_defined_information = item + .router_data + .request + .metadata + .clone() + .map(Vec::::foreign_from); + Ok(Self { + processing_information: ProcessingInformation { + capture_options: Some(CaptureOptions { + capture_sequence_number: 1, + total_capture_count: 1, + }), + action_list: None, + action_token_types: None, + authorization_options: None, + capture: None, + commerce_indicator: String::from("internet"), + payment_solution: None, + }, + order_information: OrderInformationWithBill { + amount_details: Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency, + }, + bill_to: None, + }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }, + merchant_defined_information, + }) + } +} + +impl TryFrom<&WellsfargoRouterData<&types::PaymentsIncrementalAuthorizationRouterData>> + for WellsfargoPaymentsIncrementalAuthorizationRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &WellsfargoRouterData<&types::PaymentsIncrementalAuthorizationRouterData>, + ) -> Result { + Ok(Self { + processing_information: ProcessingInformation { + action_list: None, + action_token_types: None, + authorization_options: Some(WellsfargoAuthorizationOptions { + initiator: Some(WellsfargoPaymentInitiator { + initiator_type: None, + credential_stored_on_file: None, + stored_credential_used: Some(true), + }), + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: Some("5".to_owned()), + previous_transaction_id: None, + original_authorized_amount: None, + }), + }), + commerce_indicator: String::from("internet"), + capture: None, + capture_options: None, + payment_solution: None, + }, + order_information: OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount { + additional_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), + }, + }, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoVoidRequest { + client_reference_information: ClientReferenceInformation, + reversal_information: ReversalInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, + // The connector documentation does not mention the merchantDefinedInformation field for Void requests. But this has been still added because it works! +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReversalInformation { + amount_details: Amount, + reason: String, +} + +impl TryFrom<&WellsfargoRouterData<&types::PaymentsCancelRouterData>> for WellsfargoVoidRequest { + type Error = error_stack::Report; + fn try_from( + value: &WellsfargoRouterData<&types::PaymentsCancelRouterData>, + ) -> Result { + let merchant_defined_information = value + .router_data + .request + .metadata + .clone() + .map(Vec::::foreign_from); + Ok(Self { + client_reference_information: ClientReferenceInformation { + code: Some(value.router_data.connector_request_reference_id.clone()), + }, + reversal_information: ReversalInformation { + amount_details: Amount { + total_amount: value.amount.to_owned(), + currency: value.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + }, + )?, + }, + reason: value + .router_data + .request + .cancellation_reason + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Cancellation Reason", + })?, + }, + merchant_defined_information, + }) + } +} + pub struct WellsfargoAuthType { pub(super) api_key: Secret, + pub(super) merchant_account: Secret, + pub(super) api_secret: Secret, } impl TryFrom<&types::ConnectorAuthType> for WellsfargoAuthType { type Error = error_stack::Report; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { - match auth_type { - types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + if let types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } = auth_type + { + Ok(Self { api_key: api_key.to_owned(), - }), - _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + merchant_account: key1.to_owned(), + api_secret: api_secret.to_owned(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum WellsfargoPaymentStatus { + Authorized, Succeeded, Failed, - #[default] - Processing, + Voided, + Reversed, + Pending, + Declined, + Rejected, + Challenge, + AuthorizedPendingReview, + AuthorizedRiskDeclined, + Transmitted, + InvalidRequest, + ServerError, + PendingAuthentication, + PendingReview, + Accepted, + Cancelled, + StatusNotReceived, + //PartialAuthorized, not being consumed yet. +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum WellsfargoIncrementalAuthorizationStatus { + Authorized, + Declined, + AuthorizedPendingReview, +} + +impl ForeignFrom<(WellsfargoPaymentStatus, bool)> for enums::AttemptStatus { + fn foreign_from((status, capture): (WellsfargoPaymentStatus, bool)) -> Self { + match status { + WellsfargoPaymentStatus::Authorized + | WellsfargoPaymentStatus::AuthorizedPendingReview => { + if capture { + // Because Wellsfargo will return Payment Status as Authorized even in AutoCapture Payment + Self::Charged + } else { + Self::Authorized + } + } + WellsfargoPaymentStatus::Pending => { + if capture { + Self::Charged + } else { + Self::Pending + } + } + WellsfargoPaymentStatus::Succeeded | WellsfargoPaymentStatus::Transmitted => { + Self::Charged + } + WellsfargoPaymentStatus::Voided + | WellsfargoPaymentStatus::Reversed + | WellsfargoPaymentStatus::Cancelled => Self::Voided, + WellsfargoPaymentStatus::Failed + | WellsfargoPaymentStatus::Declined + | WellsfargoPaymentStatus::AuthorizedRiskDeclined + | WellsfargoPaymentStatus::Rejected + | WellsfargoPaymentStatus::InvalidRequest + | WellsfargoPaymentStatus::ServerError => Self::Failure, + WellsfargoPaymentStatus::PendingAuthentication => Self::AuthenticationPending, + WellsfargoPaymentStatus::PendingReview + | WellsfargoPaymentStatus::StatusNotReceived + | WellsfargoPaymentStatus::Challenge + | WellsfargoPaymentStatus::Accepted => Self::Pending, + } + } } -impl From for enums::AttemptStatus { - fn from(item: WellsfargoPaymentStatus) -> Self { +impl From for common_enums::AuthorizationStatus { + fn from(item: WellsfargoIncrementalAuthorizationStatus) -> Self { match item { - WellsfargoPaymentStatus::Succeeded => Self::Charged, - WellsfargoPaymentStatus::Failed => Self::Failure, - WellsfargoPaymentStatus::Processing => Self::Authorizing, + WellsfargoIncrementalAuthorizationStatus::Authorized + | WellsfargoIncrementalAuthorizationStatus::AuthorizedPendingReview => Self::Success, + WellsfargoIncrementalAuthorizationStatus::Declined => Self::Failure, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct WellsfargoPaymentsResponse { - status: WellsfargoPaymentStatus, id: String, + status: Option, + client_reference_information: Option, + processor_information: Option, + risk_information: Option, + token_information: Option, + error_information: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoErrorInformationResponse { + id: String, + error_information: WellsfargoErrorInformation, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoPaymentsIncrementalAuthorizationResponse { + status: WellsfargoIncrementalAuthorizationStatus, + error_information: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientReferenceInformation { + code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientProcessorInformation { + network_transaction_id: Option, + avs: Option, + card_verification: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardVerification { + result_code: Option, + result_code_raw: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Avs { + code: Option, + code_raw: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientRiskInformation { + rules: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ClientRiskInformationRules { + name: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoTokenInformation { + payment_instrument: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WellsfargoErrorInformation { + reason: Option, + message: Option, + details: Option>, } impl - TryFrom< + ForeignFrom<( + &WellsfargoErrorInformationResponse, types::ResponseRouterData, - > for types::RouterData + Option, + )> for types::RouterData +{ + fn foreign_from( + (error_response, item, transaction_status): ( + &WellsfargoErrorInformationResponse, + types::ResponseRouterData< + F, + WellsfargoPaymentsResponse, + T, + types::PaymentsResponseData, + >, + Option, + ), + ) -> Self { + let detailed_error_info = + error_response + .error_information + .details + .to_owned() + .map(|details| { + details + .iter() + .map(|details| format!("{} : {}", details.field, details.reason)) + .collect::>() + .join(", ") + }); + + let reason = get_error_reason( + error_response.error_information.message.clone(), + detailed_error_info, + None, + ); + let response = Err(types::ErrorResponse { + code: error_response + .error_information + .reason + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_response + .error_information + .reason + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + match transaction_status { + Some(status) => Self { + response, + status, + ..item.data + }, + None => Self { + response, + ..item.data + }, + } + } +} + +fn get_error_response_if_failure( + (info_response, status, http_code): (&WellsfargoPaymentsResponse, enums::AttemptStatus, u16), +) -> Option { + if utils::is_payment_failure(status) { + Some(types::ErrorResponse::foreign_from(( + &info_response.error_information, + &info_response.risk_information, + Some(status), + http_code, + info_response.id.clone(), + ))) + } else { + None + } +} + +fn get_payment_response( + (info_response, status, http_code): (&WellsfargoPaymentsResponse, enums::AttemptStatus, u16), +) -> Result { + let error_response = get_error_response_if_failure((info_response, status, http_code)); + match error_response { + Some(error) => Err(error), + None => { + let incremental_authorization_allowed = + Some(status == enums::AttemptStatus::Authorized); + let mandate_reference = + info_response + .token_information + .clone() + .map(|token_info| types::MandateReference { + connector_mandate_id: token_info + .payment_instrument + .map(|payment_instrument| payment_instrument.id.expose()), + payment_method_id: None, + }); + + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: info_response.processor_information.as_ref().and_then( + |processor_information| processor_information.network_transaction_id.clone(), + ), + connector_response_reference_id: Some( + info_response + .client_reference_information + .clone() + .and_then(|client_reference_information| client_reference_information.code) + .unwrap_or(info_response.id.clone()), + ), + incremental_authorization_allowed, + charge_id: None, + }) + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + api::Authorize, + WellsfargoPaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + api::Authorize, + WellsfargoPaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + let status = enums::AttemptStatus::foreign_from(( + item.response + .status + .clone() + .unwrap_or(WellsfargoPaymentStatus::StatusNotReceived), + item.data.request.is_auto_capture()?, + )); + let response = get_payment_response((&item.response, status, item.http_code)); + let connector_response = item + .response + .processor_information + .as_ref() + .map(types::AdditionalPaymentMethodConnectorResponse::from) + .map(types::ConnectorResponseData::with_additional_payment_method_data); + + Ok(Self { + status, + response, + connector_response, + ..item.data + }) + } +} + +impl From<&ClientProcessorInformation> for types::AdditionalPaymentMethodConnectorResponse { + fn from(processor_information: &ClientProcessorInformation) -> Self { + let payment_checks = Some( + serde_json::json!({"avs_response": processor_information.avs, "card_verification": processor_information.card_verification}), + ); + + Self::Card { + authentication_data: None, + payment_checks, + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + WellsfargoPaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, WellsfargoPaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + ) -> Result { + let status = enums::AttemptStatus::foreign_from(( + item.response + .status + .clone() + .unwrap_or(WellsfargoPaymentStatus::StatusNotReceived), + true, + )); + let response = get_payment_response((&item.response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + WellsfargoPaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + WellsfargoPaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + ) -> Result { + let status = enums::AttemptStatus::foreign_from(( + item.response + .status + .clone() + .unwrap_or(WellsfargoPaymentStatus::StatusNotReceived), + false, + )); + let response = get_payment_response((&item.response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } +} + +// zero dollar response +impl + TryFrom< + types::ResponseRouterData< + api::SetupMandate, + WellsfargoPaymentsResponse, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + > + for types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + api::SetupMandate, + WellsfargoPaymentsResponse, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + ) -> Result { + let mandate_reference = + item.response + .token_information + .clone() + .map(|token_info| types::MandateReference { + connector_mandate_id: token_info + .payment_instrument + .map(|payment_instrument| payment_instrument.id.expose()), + payment_method_id: None, + }); + let mut mandate_status = enums::AttemptStatus::foreign_from(( + item.response + .status + .clone() + .unwrap_or(WellsfargoPaymentStatus::StatusNotReceived), + false, + )); + if matches!(mandate_status, enums::AttemptStatus::Authorized) { + //In case of zero auth mandates we want to make the payment reach the terminal status so we are converting the authorized status to charged as well. + mandate_status = enums::AttemptStatus::Charged + } + let error_response = + get_error_response_if_failure((&item.response, mandate_status, item.http_code)); + + let connector_response = item + .response + .processor_information + .as_ref() + .map(types::AdditionalPaymentMethodConnectorResponse::from) + .map(types::ConnectorResponseData::with_additional_payment_method_data); + + Ok(Self { + status: mandate_status, + response: match error_response { + Some(error) => Err(error), + None => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.id.clone(), + ), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: item.response.processor_information.as_ref().and_then( + |processor_information| { + processor_information.network_transaction_id.clone() + }, + ), + connector_response_reference_id: Some( + item.response + .client_reference_information + .and_then(|client_reference_information| { + client_reference_information.code.clone() + }) + .unwrap_or(item.response.id), + ), + incremental_authorization_allowed: Some( + mandate_status == enums::AttemptStatus::Authorized, + ), + charge_id: None, + }), + }, + connector_response, + ..item.data + }) + } +} + +impl + ForeignTryFrom<( + types::ResponseRouterData< + F, + WellsfargoPaymentsIncrementalAuthorizationResponse, T, types::PaymentsResponseData, >, + bool, + )> for types::RouterData +{ + type Error = error_stack::Report; + fn foreign_try_from( + data: ( + types::ResponseRouterData< + F, + WellsfargoPaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, + bool, + ), ) -> Result { + let item = data.0; Ok(Self { - status: enums::AttemptStatus::from(item.response.status), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), + response: match item.response.error_information { + Some(error) => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus::Failure, + error_code: error.reason, + error_message: error.message, + connector_authorization_id: None, + }, + ), + _ => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: item.response.status.into(), + error_code: None, + error_message: None, + connector_authorization_id: None, + }, + ), + }, ..item.data }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoTransactionResponse { + id: String, + application_information: ApplicationInformation, + client_reference_information: Option, + error_information: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplicationInformation { + status: Option, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + WellsfargoTransactionResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + WellsfargoTransactionResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response.application_information.status { + Some(status) => { + let status = enums::AttemptStatus::foreign_from(( + status, + item.data.request.is_auto_capture()?, + )); + let incremental_authorization_allowed = + Some(status == enums::AttemptStatus::Authorized); + let risk_info: Option = None; + if utils::is_payment_failure(status) { + Ok(Self { + response: Err(types::ErrorResponse::foreign_from(( + &item.response.error_information, + &risk_info, + Some(status), + item.http_code, + item.response.id.clone(), + ))), + status: enums::AttemptStatus::Failure, + ..item.data + }) + } else { + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: item + .response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed, + charge_id: None, + }), + ..item.data + }) + } + } + None => Ok(Self { + status: item.data.status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct WellsfargoRefundRequest { - pub amount: StringMinorUnit, + order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, } impl TryFrom<&WellsfargoRouterData<&types::RefundsRouterData>> for WellsfargoRefundRequest { @@ -156,79 +2177,319 @@ impl TryFrom<&WellsfargoRouterData<&types::RefundsRouterData>> for Wellsfa item: &WellsfargoRouterData<&types::RefundsRouterData>, ) -> Result { Ok(Self { - amount: item.amount.to_owned(), + order_information: OrderInformation { + amount_details: Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency, + }, + }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.request.refund_id.clone()), + }, }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: WellsfargoRefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + WellsfargoRefundStatus::Succeeded | WellsfargoRefundStatus::Transmitted => { + Self::Success + } + WellsfargoRefundStatus::Cancelled + | WellsfargoRefundStatus::Failed + | WellsfargoRefundStatus::Voided => Self::Failure, + WellsfargoRefundStatus::Pending => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum WellsfargoRefundStatus { + Succeeded, + Transmitted, + Failed, + Pending, + Voided, + Cancelled, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoRefundResponse { id: String, - status: RefundStatus, + status: WellsfargoRefundStatus, + error_information: Option, } -impl TryFrom> +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), + let refund_status = enums::RefundStatus::from(item.response.status.clone()); + let response = if utils::is_refund_failure(refund_status) { + Err(types::ErrorResponse::foreign_from(( + &item.response.error_information, + &None, + None, + item.http_code, + item.response.id.clone(), + ))) + } else { + Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, refund_status: enums::RefundStatus::from(item.response.status), - }), + }) + }; + + Ok(Self { + response, ..item.data }) } } -impl TryFrom> +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RsyncApplicationInformation { + status: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoRsyncResponse { + id: String, + application_information: Option, + error_information: Option, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + let response = match item + .response + .application_information + .and_then(|application_information| application_information.status) + { + Some(status) => { + let refund_status = enums::RefundStatus::from(status.clone()); + if utils::is_refund_failure(refund_status) { + if status == WellsfargoRefundStatus::Voided { + Err(types::ErrorResponse::foreign_from(( + &Some(WellsfargoErrorInformation { + message: Some(consts::REFUND_VOIDED.to_string()), + reason: Some(consts::REFUND_VOIDED.to_string()), + details: None, + }), + &None, + None, + item.http_code, + item.response.id.clone(), + ))) + } else { + Err(types::ErrorResponse::foreign_from(( + &item.response.error_information, + &None, + None, + item.http_code, + item.response.id.clone(), + ))) + } + } else { + Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, + refund_status, + }) + } + } + + None => Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.clone(), + refund_status: match item.data.response { + Ok(response) => response.refund_status, + Err(_) => common_enums::RefundStatus::Pending, + }, }), + }; + + Ok(Self { + response, ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct WellsfargoErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoStandardErrorResponse { + pub error_information: Option, + pub status: Option, + pub message: Option, pub reason: Option, + pub details: Option>, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoNotAvailableErrorResponse { + pub errors: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoNotAvailableErrorObject { + #[serde(rename = "type")] + pub error_type: Option, + pub message: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WellsfargoServerErrorResponse { + pub status: Option, + pub message: Option, + pub reason: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Reason { + SystemError, + ServerTimeout, + ServiceTimeout, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct WellsfargoAuthenticationErrorResponse { + pub response: AuthenticationErrorInformation, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum WellsfargoErrorResponse { + AuthenticationError(Box), + //If the request resource is not available/exists in wellsfargo + NotAvailableError(Box), + StandardError(Box), +} + +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Details { + pub field: String, + pub reason: String, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct ErrorInformation { + pub message: String, + pub reason: String, + pub details: Option>, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct AuthenticationErrorInformation { + pub rmsg: String, +} + +impl + ForeignFrom<( + &Option, + &Option, + Option, + u16, + String, + )> for types::ErrorResponse +{ + fn foreign_from( + (error_data, risk_information, attempt_status, status_code, transaction_id): ( + &Option, + &Option, + Option, + u16, + String, + ), + ) -> Self { + let avs_message = risk_information + .clone() + .map(|client_risk_information| { + client_risk_information.rules.map(|rules| { + rules + .iter() + .map(|risk_info| { + risk_info.name.clone().map_or("".to_string(), |name| { + format!(" , {}", name.clone().expose()) + }) + }) + .collect::>() + .join("") + }) + }) + .unwrap_or(Some("".to_string())); + + let detailed_error_info = error_data + .clone() + .map(|error_data| match error_data.details { + Some(details) => details + .iter() + .map(|details| format!("{} : {}", details.field, details.reason)) + .collect::>() + .join(", "), + None => "".to_string(), + }); + + let reason = get_error_reason( + error_data.clone().and_then(|error_info| error_info.message), + detailed_error_info, + avs_message, + ); + let error_message = error_data.clone().and_then(|error_info| error_info.reason); + Self { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason, + status_code, + attempt_status, + connector_transaction_id: Some(transaction_id.clone()), + } + } +} + +pub fn get_error_reason( + error_info: Option, + detailed_error_info: Option, + avs_error_info: Option, +) -> Option { + match (error_info, detailed_error_info, avs_error_info) { + (Some(message), Some(details), Some(avs_message)) => Some(format!( + "{}, detailed_error_information: {}, avs_message: {}", + message, details, avs_message + )), + (Some(message), Some(details), None) => Some(format!( + "{}, detailed_error_information: {}", + message, details + )), + (Some(message), None, Some(avs_message)) => { + Some(format!("{}, avs_message: {}", message, avs_message)) + } + (None, Some(details), Some(avs_message)) => { + Some(format!("{}, avs_message: {}", details, avs_message)) + } + (Some(message), None, None) => Some(message), + (None, Some(details), None) => Some(details), + (None, None, Some(avs_message)) => Some(avs_message), + (None, None, None) => None, + } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index ed1ee3a6c6af..820a0a965c63 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -7,7 +7,7 @@ use api_models::{ use base64::Engine; use common_utils::{ date_time, - ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, + ext_traits::{AsyncExt, Encode, ValueExt}, id_type, pii, types::keymanager::{self as km_types, KeyManagerState}, }; @@ -15,7 +15,6 @@ use diesel_models::configs; #[cfg(all(any(feature = "v1", feature = "v2"), feature = "olap"))] use diesel_models::organization::OrganizationBridge; use error_stack::{report, FutureExt, ResultExt}; -use futures::future::try_join_all; use masking::{ExposeInterface, PeekInterface, Secret}; use pm_auth::{connector::plaid::transformers::PlaidAuthType, types as pm_auth_types}; use regex::Regex; @@ -230,7 +229,7 @@ pub async fn create_merchant_account( }; let domain_merchant_account = req - .create_domain_model_from_request(&state, key_store.clone()) + .create_domain_model_from_request(&state, key_store.clone(), &merchant_id) .await?; let key_manager_state = &(&state).into(); db.insert_merchant_key_store( @@ -264,6 +263,7 @@ trait MerchantAccountCreateBridge { self, state: &SessionState, key: domain::MerchantKeyStore, + identifier: &id_type::MerchantId, ) -> RouterResult; } @@ -278,6 +278,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { self, state: &SessionState, key_store: domain::MerchantKeyStore, + identifier: &id_type::MerchantId, ) -> RouterResult { let db = &*state.store; let publishable_key = create_merchant_publishable_key(); @@ -337,7 +338,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { let merchant_account = async { Ok::<_, error_stack::Report>( domain::MerchantAccountSetter { - merchant_id: self.merchant_id, + merchant_id: identifier.clone(), merchant_name: self .merchant_name .async_lift(|inner| { @@ -614,6 +615,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { self, state: &SessionState, key_store: domain::MerchantKeyStore, + identifier: &id_type::MerchantId, ) -> RouterResult { let publishable_key = create_merchant_publishable_key(); let db = &*state.store; @@ -630,18 +632,12 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { }, )?; - let primary_business_details = self.get_primary_details_as_value().change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "primary_business_details", - }, - )?; - let organization = CreateOrValidateOrganization::new(self.organization_id.clone()) .create_or_validate(db) .await?; let key = key_store.key.into_inner(); - let id = self.get_merchant_reference_id().to_owned(); + let id = identifier.to_owned(); let key_manager_state = state.into(); let identifier = km_types::Identifier::Merchant(id.clone()); @@ -669,33 +665,19 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { ) }) .await?, - return_url: None, - webhook_details: None, routing_algorithm: Some(serde_json::json!({ "algorithm_id": null, "timestamp": 0 })), - sub_merchants_enabled: None, - parent_merchant_id: None, - enable_payment_response_hash: true, - payment_response_hash_key: None, - redirect_to_merchant_with_http_post: true, publishable_key, - locker_id: None, metadata, storage_scheme: MerchantStorageScheme::PostgresOnly, - primary_business_details, created_at: date_time::now(), modified_at: date_time::now(), - intent_fulfillment_time: None, frm_routing_algorithm: None, payout_routing_algorithm: None, organization_id: organization.get_organization_id(), - is_recon_enabled: false, - default_profile: None, recon_status: diesel_models::enums::ReconStatus::NotRequested, - payment_link_config: None, - pm_collect_link_config: None, }), ) } @@ -758,7 +740,10 @@ pub async fn get_merchant_account( )) } -#[cfg(any(feature = "v1", feature = "v2"))] +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] /// For backwards compatibility, whenever new business labels are passed in /// primary_business_details, create a business profile pub async fn create_business_profile_from_business_labels( @@ -828,74 +813,218 @@ pub async fn create_business_profile_from_business_labels( Ok(()) } -/// For backwards compatibility -/// If any of the fields of merchant account are updated, then update these fields in business profiles -pub async fn update_business_profile_cascade( - state: SessionState, - merchant_account_update: api::MerchantAccountUpdate, - merchant_id: id_type::MerchantId, -) -> RouterResult<()> { - if merchant_account_update.return_url.is_some() - || merchant_account_update.webhook_details.is_some() - || merchant_account_update - .enable_payment_response_hash - .is_some() - || merchant_account_update - .redirect_to_merchant_with_http_post - .is_some() - { - // Update these fields in all the business profiles - let business_profiles = state - .store - .list_business_profile_by_merchant_id(&merchant_id) - .await - .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { - id: merchant_id.get_string_repr().to_owned(), - })?; +#[cfg(any(feature = "v1", feature = "v2", feature = "olap"))] +#[async_trait::async_trait] +trait MerchantAccountUpdateBridge { + async fn get_update_merchant_object( + self, + state: &SessionState, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + ) -> RouterResult; +} - let business_profile_update = admin_types::BusinessProfileUpdate { - profile_name: None, - return_url: merchant_account_update.return_url, - enable_payment_response_hash: merchant_account_update.enable_payment_response_hash, - payment_response_hash_key: merchant_account_update.payment_response_hash_key, - redirect_to_merchant_with_http_post: merchant_account_update - .redirect_to_merchant_with_http_post, - webhook_details: merchant_account_update.webhook_details, - metadata: None, - routing_algorithm: None, +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] +#[async_trait::async_trait] +impl MerchantAccountUpdateBridge for api::MerchantAccountUpdate { + async fn get_update_merchant_object( + self, + state: &SessionState, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + use common_utils::ext_traits::ConfigExt; + + let key_manager_state = &state.into(); + let key = key_store.key.get_inner().peek(); + + let db = state.store.as_ref(); + + let primary_business_details = self.get_primary_details_as_value().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "primary_business_details", + }, + )?; + + let pm_collect_link_config = self.get_pm_link_config_as_value().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "pm_collect_link_config", + }, + )?; + + let merchant_details = self.get_merchant_details_as_secret().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_details", + }, + )?; + + self.parse_routing_algorithm().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "routing_algorithm", + }, + )?; + + let webhook_details = self.webhook_details.map(ForeignInto::foreign_into); + + let parent_merchant_id = get_parent_merchant( + state, + self.sub_merchants_enabled, + self.parent_merchant_id.as_ref(), + key_store, + ) + .await?; + + // This supports changing the business profile by passing in the profile_id + let business_profile_id_update = if let Some(ref profile_id) = self.default_profile { + if !profile_id.is_empty_after_trim() { + // Validate whether profile_id passed in request is valid and is linked to the merchant + core_utils::validate_and_get_business_profile( + state.store.as_ref(), + Some(profile_id), + merchant_id, + ) + .await? + .map(|business_profile| Some(business_profile.profile_id)) + } else { + // If empty, Update profile_id to None in the database + Some(None) + } + } else { + None + }; + + #[cfg(any(feature = "v1", feature = "v2"))] + // In order to support backwards compatibility, if a business_labels are passed in the update + // call, then create new business_profiles with the profile_name as business_label + self.primary_business_details + .clone() + .async_map(|primary_business_details| async { + let _ = create_business_profile_from_business_labels( + state, + db, + key_store, + merchant_id, + primary_business_details, + ) + .await; + }) + .await; + + let identifier = km_types::Identifier::Merchant(key_store.merchant_id.clone()); + Ok(storage::MerchantAccountUpdate::Update { + merchant_name: self + .merchant_name + .map(Secret::new) + .async_lift(|inner| { + domain_types::encrypt_optional( + key_manager_state, + inner, + identifier.clone(), + key, + ) + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt merchant name")?, + merchant_details: merchant_details + .async_lift(|inner| { + domain_types::encrypt_optional( + key_manager_state, + inner, + km_types::Identifier::Merchant(key_store.merchant_id.clone()), + key, + ) + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt merchant details")?, + return_url: self.return_url.map(|a| a.to_string()), + webhook_details, + sub_merchants_enabled: self.sub_merchants_enabled, + parent_merchant_id, + enable_payment_response_hash: self.enable_payment_response_hash, + payment_response_hash_key: self.payment_response_hash_key, + redirect_to_merchant_with_http_post: self.redirect_to_merchant_with_http_post, + locker_id: self.locker_id, + metadata: self.metadata, + publishable_key: None, + primary_business_details, + frm_routing_algorithm: self.frm_routing_algorithm, intent_fulfillment_time: None, - frm_routing_algorithm: None, #[cfg(feature = "payouts")] + payout_routing_algorithm: self.payout_routing_algorithm, + #[cfg(not(feature = "payouts"))] payout_routing_algorithm: None, - applepay_verified_domains: None, + default_profile: business_profile_id_update, payment_link_config: None, - session_expiry: None, - authentication_connector_details: None, - payout_link_config: None, - extended_card_info_config: None, - use_billing_as_payment_method_billing: None, - collect_shipping_details_from_wallet_connector: None, - collect_billing_details_from_wallet_connector: None, - is_connector_agnostic_mit_enabled: None, - outgoing_webhook_custom_http_headers: None, - }; + pm_collect_link_config, + routing_algorithm: self.routing_algorithm, + }) + } +} - let update_futures = business_profiles.iter().map(|business_profile| async { - let profile_id = &business_profile.profile_id; +#[cfg(all(any(feature = "v1", feature = "v2"), feature = "merchant_account_v2",))] +#[async_trait::async_trait] +impl MerchantAccountUpdateBridge for api::MerchantAccountUpdate { + async fn get_update_merchant_object( + self, + state: &SessionState, + _merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + let key_manager_state = &state.into(); + let key = key_store.key.get_inner().peek(); - update_business_profile( - state.clone(), - profile_id, - &merchant_id, - business_profile_update.clone(), - ) - .await - }); + let merchant_details = self.get_merchant_details_as_secret().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_details", + }, + )?; - try_join_all(update_futures).await?; - } + let metadata = self.get_metadata_as_secret().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "metadata", + }, + )?; - Ok(()) + let identifier = km_types::Identifier::Merchant(key_store.merchant_id.clone()); + Ok(storage::MerchantAccountUpdate::Update { + merchant_name: self + .merchant_name + .map(Secret::new) + .async_lift(|inner| { + domain_types::encrypt_optional( + key_manager_state, + inner, + identifier.clone(), + key, + ) + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt merchant name")?, + merchant_details: merchant_details + .async_lift(|inner| { + domain_types::encrypt_optional( + key_manager_state, + inner, + km_types::Identifier::Merchant(key_store.merchant_id.clone()), + key, + ) + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt merchant details")?, + metadata, + publishable_key: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + routing_algorithm: None, + }) + } } pub async fn merchant_account_update( @@ -909,164 +1038,27 @@ pub async fn merchant_account_update( let key_store = db .get_merchant_key_store_by_merchant_id( key_manager_state, - &req.merchant_id, + merchant_id, &db.get_master_key().to_vec().into(), ) .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - if &req.merchant_id != merchant_id { - Err(report!(errors::ValidationError::IncorrectValueProvided { - field_name: "parent_merchant_id" - }) - .attach_printable( - "If `sub_merchants_enabled` is true, then `parent_merchant_id` is mandatory", - ) - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "parent_merchant_id", - }))?; - } - - if let Some(ref routing_algorithm) = req.routing_algorithm { - let _: api_models::routing::RoutingAlgorithm = routing_algorithm - .clone() - .parse_value("RoutingAlgorithm") - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "routing_algorithm", - }) - .attach_printable("Invalid routing algorithm given")?; - } - - let primary_business_details = req - .primary_business_details - .as_ref() - .map(|primary_business_details| { - primary_business_details.encode_to_value().change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "primary_business_details", - }, - ) - }) - .transpose()?; - - let pm_collect_link_config = req - .pm_collect_link_config - .as_ref() - .map(|c| { - c.encode_to_value() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "pm_collect_link_config", - }) - }) - .transpose()?; - - #[cfg(any(feature = "v1", feature = "v2"))] - // In order to support backwards compatibility, if a business_labels are passed in the update - // call, then create new business_profiles with the profile_name as business_label - req.primary_business_details - .clone() - .async_map(|primary_business_details| async { - let _ = create_business_profile_from_business_labels( - &state, - db, - &key_store, - merchant_id, - primary_business_details, - ) - .await; - }) - .await; - - let key = key_store.key.get_inner().peek(); - - let business_profile_id_update = if let Some(ref profile_id) = req.default_profile { - if !profile_id.is_empty_after_trim() { - // Validate whether profile_id passed in request is valid and is linked to the merchant - core_utils::validate_and_get_business_profile(db, Some(profile_id), merchant_id) - .await? - .map(|business_profile| Some(business_profile.profile_id)) - } else { - // If empty, Update profile_id to None in the database - Some(None) - } - } else { - None - }; - - // Update the business profile, This is for backwards compatibility - update_business_profile_cascade(state.clone(), req.clone(), merchant_id.to_owned()).await?; - - let identifier = km_types::Identifier::Merchant(key_store.merchant_id.clone()); - let updated_merchant_account = storage::MerchantAccountUpdate::Update { - merchant_name: req - .merchant_name - .map(Secret::new) - .async_lift(|inner| { - domain_types::encrypt_optional(key_manager_state, inner, identifier.clone(), key) - }) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt merchant name")?, - - merchant_details: req - .merchant_details - .as_ref() - .map(Encode::encode_to_value) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to convert merchant_details to a value")? - .map(Secret::new) - .async_lift(|inner| { - domain_types::encrypt_optional(key_manager_state, inner, identifier.clone(), key) - }) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt merchant details")?, - - return_url: req.return_url.map(|a| a.to_string()), - - webhook_details: req.webhook_details.map(ForeignInto::foreign_into), - - routing_algorithm: req.routing_algorithm, - sub_merchants_enabled: req.sub_merchants_enabled, - - parent_merchant_id: get_parent_merchant( - &state, - req.sub_merchants_enabled, - req.parent_merchant_id.as_ref(), - &key_store, - ) - .await?, - enable_payment_response_hash: req.enable_payment_response_hash, - payment_response_hash_key: req.payment_response_hash_key, - redirect_to_merchant_with_http_post: req.redirect_to_merchant_with_http_post, - locker_id: req.locker_id, - metadata: req.metadata, - publishable_key: None, - primary_business_details, - frm_routing_algorithm: req.frm_routing_algorithm, - intent_fulfillment_time: None, - #[cfg(feature = "payouts")] - payout_routing_algorithm: req.payout_routing_algorithm, - #[cfg(not(feature = "payouts"))] - payout_routing_algorithm: None, - default_profile: business_profile_id_update, - payment_link_config: None, - pm_collect_link_config, - }; + let merchant_account_storage_object = req + .get_update_merchant_object(&state, merchant_id, &key_store) + .await + .attach_printable("Failed to create merchant account update object")?; let response = db .update_specific_fields_in_merchant( key_manager_state, merchant_id, - updated_merchant_account, + merchant_account_storage_object, &key_store, ) .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - // If there are any new business labels generated, create business profile - Ok(service_api::ApplicationResponse::Json( api::MerchantAccountResponse::foreign_try_from(response) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1144,6 +1136,10 @@ pub async fn merchant_account_delete( Ok(service_api::ApplicationResponse::Json(response)) } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] async fn get_parent_merchant( state: &SessionState, sub_merchants_enabled: Option, @@ -1172,6 +1168,10 @@ async fn get_parent_merchant( }) } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] async fn validate_merchant_id( state: &SessionState, merchant_id: &id_type::MerchantId, @@ -1290,9 +1290,7 @@ impl<'a> ConnectorAuthTypeAndMetadataValidation<'a> { } api_enums::Connector::Braintree => { braintree::transformers::BraintreeAuthType::try_from(self.auth_type)?; - braintree::braintree_graphql_transformers::BraintreeMeta::try_from( - self.connector_meta_data, - )?; + braintree::transformers::BraintreeMeta::try_from(self.connector_meta_data)?; Ok(()) } api_enums::Connector::Cashtocode => { @@ -1478,10 +1476,10 @@ impl<'a> ConnectorAuthTypeAndMetadataValidation<'a> { volt::transformers::VoltAuthType::try_from(self.auth_type)?; Ok(()) } - // api_enums::Connector::Wellsfargo => { - // wellsfargo::transformers::WellsfargoAuthType::try_from(self.auth_type)?; - // Ok(()) - // } + api_enums::Connector::Wellsfargo => { + wellsfargo::transformers::WellsfargoAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Wise => { wise::transformers::WiseAuthType::try_from(self.auth_type)?; Ok(()) @@ -2200,7 +2198,8 @@ trait MerchantConnectorAccountCreateBridge { #[cfg(all( feature = "v2", feature = "merchant_connector_account_v2", - feature = "olap" + feature = "olap", + feature = "merchant_account_v2" ))] #[async_trait::async_trait] impl MerchantConnectorAccountCreateBridge for api::MerchantConnectorCreate { @@ -2352,7 +2351,8 @@ impl MerchantConnectorAccountCreateBridge for api::MerchantConnectorCreate { #[cfg(all( any(feature = "v1", feature = "v2", feature = "olap"), - not(feature = "merchant_connector_account_v2") + not(feature = "merchant_connector_account_v2"), + not(feature = "merchant_account_v2") ))] #[async_trait::async_trait] impl MerchantConnectorAccountCreateBridge for api::MerchantConnectorCreate { @@ -2573,7 +2573,7 @@ pub async fn create_payment_connector( #[cfg(all( any(feature = "v1", feature = "v2"), - not(feature = "merchant_connector_account_v2") + not(feature = "merchant_account_v2") ))] helpers::validate_business_details( req.business_country, @@ -3150,9 +3150,19 @@ pub async fn create_and_insert_business_profile( merchant_account: domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] let business_profile_new = admin::create_business_profile(state, merchant_account, request, key_store).await?; + #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + let business_profile_new = { + let _ = merchant_account; + admin::create_business_profile(state, request, key_store).await? + }; + let profile_name = business_profile_new.profile_name.clone(); state @@ -3167,6 +3177,10 @@ pub async fn create_and_insert_business_profile( .attach_printable("Failed to insert Business profile because of duplication error") } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] pub async fn create_business_profile( state: SessionState, request: api::BusinessProfileCreate, @@ -3232,6 +3246,15 @@ pub async fn create_business_profile( )) } +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] +pub async fn create_business_profile( + _state: SessionState, + _request: api::BusinessProfileCreate, + _merchant_id: &id_type::MerchantId, +) -> RouterResponse { + todo!() +} + pub async fn list_business_profile( state: SessionState, merchant_id: id_type::MerchantId, diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index df3a5a2ca2f9..64fadd0e33d1 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -2,6 +2,7 @@ use std::fmt::Debug; use api_models::{admin::FrmConfigs, enums as api_enums}; use common_enums::CaptureMethod; +use common_utils::ext_traits::OptionExt; use error_stack::ResultExt; use masking::{ExposeInterface, PeekInterface}; use router_env::{ @@ -21,7 +22,6 @@ use crate::{ core::{ errors::{self, RouterResult}, payments::{self, flows::ConstructFlowSpecificData, operations::BoxedOperation}, - utils as core_utils, }, db::StorageInterface, routes::{app::ReqState, SessionState}, @@ -145,16 +145,14 @@ where }) .attach_printable("Data field not found in frm_routing_algorithm")?; - let profile_id = core_utils::get_profile_id_from_business_details( - payment_data.payment_intent.business_country, - payment_data.payment_intent.business_label.as_ref(), - merchant_account, - payment_data.payment_intent.profile_id.as_ref(), - db, - false, - ) - .await - .attach_printable("Could not find profile id from business details")?; + let profile_id = payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); #[cfg(all( any(feature = "v1", feature = "v2"), diff --git a/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs index a16dc2b16e19..c99636581c32 100644 --- a/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs +++ b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs @@ -1,4 +1,4 @@ -use common_utils::ext_traits::ValueExt; +use common_utils::ext_traits::{OptionExt, ValueExt}; use error_stack::ResultExt; use router_env::tracing::{self, instrument}; @@ -26,17 +26,13 @@ pub async fn construct_fulfillment_router_data<'a>( connector: String, fulfillment_request: FrmFulfillmentRequest, ) -> RouterResult { - let profile_id = core_utils::get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index b919541adefb..985036ec71a6 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -48,6 +48,11 @@ use super::surcharge_decision_configs::{ perform_surcharge_decision_management_for_payment_method_list, perform_surcharge_decision_management_for_saved_cards, }; +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] +use crate::routes::app::SessionStateInfo; #[cfg(feature = "payouts")] use crate::types::domain::types::AsyncLift; use crate::{ @@ -67,7 +72,7 @@ use crate::{ }, db, logger, pii::prelude::*, - routes::{self, app::SessionStateInfo, metrics, payment_methods::ParentPaymentMethodToken}, + routes::{self, metrics, payment_methods::ParentPaymentMethodToken}, services, types::{ api::{self, routing as routing_types, PaymentMethodCreateExt}, @@ -2335,16 +2340,12 @@ pub async fn list_payment_methods( let profile_id = payment_intent .as_ref() .async_map(|payment_intent| async { - crate::core::utils::get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - &merchant_account, - payment_intent.profile_id.as_ref(), - db, - false, - ) - .await - .attach_printable("Could not find profile id from business details") + payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent") }) .await .transpose()?; @@ -2667,7 +2668,7 @@ pub async fn list_payment_methods( for inner_config in config.enabled_payment_methods.iter() { let is_active_mca = all_mcas .iter() - .any(|mca| mca.merchant_connector_id == inner_config.mca_id); + .any(|mca| mca.get_id() == inner_config.mca_id); if inner_config.payment_method_type == *payment_method_type && is_active_mca { @@ -3611,6 +3612,63 @@ fn filter_recurring_based( recurring_enabled.map_or(true, |enabled| payment_method.recurring_enabled == enabled) } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn list_customer_payment_method_util( + state: routes::SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: Option, + customer_id: Option, + is_payment_associated: bool, +) -> errors::RouterResponse { + let limit = req.as_ref().and_then(|pml_req| pml_req.limit); + + let (customer_id, payment_intent) = if is_payment_associated { + let cloned_secret = req.and_then(|r| r.client_secret.clone()); + let payment_intent = helpers::verify_payment_intent_time_and_client_secret( + &state, + &merchant_account, + &key_store, + cloned_secret, + ) + .await?; + + ( + payment_intent + .as_ref() + .and_then(|pi| pi.customer_id.clone()), + payment_intent, + ) + } else { + (customer_id, None) + }; + + let resp = if let Some(cust) = customer_id { + Box::pin(list_customer_payment_method( + &state, + merchant_account, + key_store, + payment_intent, + &cust, + limit, + is_payment_associated, + )) + .await? + } else { + let response = api::CustomerPaymentMethodsListResponse { + customer_payment_methods: Vec::new(), + is_guest_customer: Some(true), + }; + services::ApplicationResponse::Json(response) + }; + + Ok(resp) +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub async fn do_list_customer_pm_fetch_customer_if_not_passed( state: routes::SessionState, merchant_account: domain::MerchantAccount, @@ -3682,6 +3740,10 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( } } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub async fn list_customer_payment_method( state: &routes::SessionState, merchant_account: domain::MerchantAccount, @@ -3738,16 +3800,12 @@ pub async fn list_customer_payment_method( let profile_id = payment_intent .as_ref() .async_map(|payment_intent| async { - core_utils::get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - &merchant_account, - payment_intent.profile_id.as_ref(), - db, - false, - ) - .await - .attach_printable("Could not find profile id from business details") + payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent") }) .await .transpose()?; @@ -3769,88 +3827,21 @@ pub async fn list_customer_payment_method( let payment_method = pm.payment_method.get_required_value("payment_method")?; - let payment_method_retrieval_context = match payment_method { - enums::PaymentMethod::Card => { - let card_details = - get_card_details_with_locker_fallback(&pm, state, &key_store).await?; - - if card_details.is_some() { - PaymentMethodListContext { - card_details, - #[cfg(feature = "payouts")] - bank_transfer_details: None, - hyperswitch_token_data: PaymentTokenData::permanent_card( - Some(pm.payment_method_id.clone()), - pm.locker_id.clone().or(Some(pm.payment_method_id.clone())), - pm.locker_id.clone().unwrap_or(pm.payment_method_id.clone()), - ), - } - } else { - continue; - } - } - - enums::PaymentMethod::BankDebit => { - // Retrieve the pm_auth connector details so that it can be tokenized - let bank_account_token_data = - get_bank_account_connector_details(state, &pm, &key_store) - .await - .unwrap_or_else(|error| { - logger::error!(?error); - None - }); - if let Some(data) = bank_account_token_data { - let token_data = PaymentTokenData::AuthBankDebit(data); - - PaymentMethodListContext { - card_details: None, - #[cfg(feature = "payouts")] - bank_transfer_details: None, - hyperswitch_token_data: token_data, - } - } else { - continue; - } - } - - enums::PaymentMethod::Wallet => PaymentMethodListContext { - card_details: None, - #[cfg(feature = "payouts")] - bank_transfer_details: None, - hyperswitch_token_data: PaymentTokenData::wallet_token( - pm.payment_method_id.clone(), - ), - }, + let pm_list_context = get_pm_list_context( + state, + &payment_method, + &key_store, + &pm, + Some(parent_payment_method_token.clone()), + true, + ) + .await?; - #[cfg(feature = "payouts")] - enums::PaymentMethod::BankTransfer => PaymentMethodListContext { - card_details: None, - bank_transfer_details: Some( - get_bank_from_hs_locker( - state, - &key_store, - &parent_payment_method_token, - &pm.customer_id, - &pm.merchant_id, - pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id), - ) - .await?, - ), - hyperswitch_token_data: PaymentTokenData::temporary_generic( - parent_payment_method_token.clone(), - ), - }, + if pm_list_context.is_none() { + continue; + } - _ => PaymentMethodListContext { - card_details: None, - #[cfg(feature = "payouts")] - bank_transfer_details: None, - hyperswitch_token_data: PaymentTokenData::temporary_generic(generate_id( - consts::ID_LENGTH, - "token", - )), - }, - }; + let pm_list_context = pm_list_context.get_required_value("PaymentMethodListContext")?; // Retrieve the masked bank details to be sent as a response let bank_details = if payment_method == enums::PaymentMethod::BankDebit { @@ -3906,7 +3897,7 @@ pub async fn list_customer_payment_method( payment_method, payment_method_type: pm.payment_method_type, payment_method_issuer: pm.payment_method_issuer, - card: payment_method_retrieval_context.card_details, + card: pm_list_context.card_details, metadata: pm.metadata, payment_method_issuer_code: pm.payment_method_issuer_code, recurring_enabled: mca_enabled, @@ -3914,7 +3905,7 @@ pub async fn list_customer_payment_method( payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), #[cfg(feature = "payouts")] - bank_transfer: payment_method_retrieval_context.bank_transfer_details, + bank_transfer: pm_list_context.bank_transfer_details, bank: bank_details, surcharge_details: None, requires_cvv, @@ -3936,15 +3927,15 @@ pub async fn list_customer_payment_method( .and_then(|b_profile| b_profile.intent_fulfillment_time) .unwrap_or(consts::DEFAULT_INTENT_FULFILLMENT_TIME); + let hyperswitch_token_data = pm_list_context + .hyperswitch_token_data + .get_required_value("PaymentTokenData")?; + ParentPaymentMethodToken::create_key_for_token(( &parent_payment_method_token, pma.payment_method, )) - .insert( - intent_fulfillment_time, - payment_method_retrieval_context.hyperswitch_token_data, - state, - ) + .insert(intent_fulfillment_time, hyperswitch_token_data, state) .await?; if let Some(metadata) = pma.metadata { @@ -3975,6 +3966,115 @@ pub async fn list_customer_payment_method( customer_payment_methods: customer_pms, is_guest_customer: payment_intent.as_ref().map(|_| false), //to return this key only when the request is tied to a payment intent }; + + Box::pin(perform_surcharge_ops( + payment_intent, + state, + merchant_account, + key_store, + business_profile, + &mut response, + )) + .await?; + + Ok(services::ApplicationResponse::Json(response)) +} + +async fn get_pm_list_context( + state: &routes::SessionState, + payment_method: &enums::PaymentMethod, + key_store: &domain::MerchantKeyStore, + pm: &diesel_models::PaymentMethod, + #[cfg(feature = "payouts")] parent_payment_method_token: Option, + #[cfg(not(feature = "payouts"))] _parent_payment_method_token: Option, + is_payment_associated: bool, +) -> Result, error_stack::Report> { + let payment_method_retrieval_context = match payment_method { + enums::PaymentMethod::Card => { + let card_details = get_card_details_with_locker_fallback(pm, state, key_store).await?; + + card_details.as_ref().map(|card| PaymentMethodListContext { + card_details: Some(card.clone()), + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: is_payment_associated.then_some( + PaymentTokenData::permanent_card( + Some(pm.payment_method_id.clone()), + pm.locker_id.clone().or(Some(pm.payment_method_id.clone())), + pm.locker_id.clone().unwrap_or(pm.payment_method_id.clone()), + ), + ), + }) + } + + enums::PaymentMethod::BankDebit => { + // Retrieve the pm_auth connector details so that it can be tokenized + let bank_account_token_data = get_bank_account_connector_details(state, pm, key_store) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + + bank_account_token_data.map(|data| { + let token_data = PaymentTokenData::AuthBankDebit(data); + + PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: is_payment_associated.then_some(token_data), + } + }) + } + + enums::PaymentMethod::Wallet => Some(PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: is_payment_associated + .then_some(PaymentTokenData::wallet_token(pm.payment_method_id.clone())), + }), + + #[cfg(feature = "payouts")] + enums::PaymentMethod::BankTransfer => Some(PaymentMethodListContext { + card_details: None, + bank_transfer_details: Some( + get_bank_from_hs_locker( + state, + key_store, + parent_payment_method_token.as_ref(), + &pm.customer_id, + &pm.merchant_id, + pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id), + ) + .await?, + ), + hyperswitch_token_data: parent_payment_method_token + .map(|token| PaymentTokenData::temporary_generic(token.clone())), + }), + + _ => Some(PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: is_payment_associated.then_some( + PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")), + ), + }), + }; + + Ok(payment_method_retrieval_context) +} + +async fn perform_surcharge_ops( + payment_intent: Option, + state: &routes::SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + business_profile: Option, + response: &mut api::CustomerPaymentMethodsListResponse, +) -> Result<(), error_stack::Report> { let payment_attempt = payment_intent .as_ref() .async_map(|payment_intent| async { @@ -3991,7 +4091,6 @@ pub async fn list_customer_payment_method( }) .await .transpose()?; - if let Some((payment_attempt, payment_intent, business_profile)) = payment_attempt .zip(payment_intent) .zip(business_profile) @@ -4004,13 +4103,355 @@ pub async fn list_customer_payment_method( &business_profile, &payment_attempt, payment_intent, - &mut response, + response, ) .await?; } + Ok(()) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +struct SavedPMLPaymentsInfo { + pub payment_intent: storage::PaymentIntent, + pub business_profile: Option, + pub requires_cvv: bool, + pub off_session_payment_flag: bool, + pub is_connector_agnostic_mit_enabled: bool, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl SavedPMLPaymentsInfo { + pub async fn form_payments_info( + payment_intent: storage::PaymentIntent, + merchant_account: &domain::MerchantAccount, + db: &dyn db::StorageInterface, + ) -> errors::RouterResult { + let requires_cvv = db + .find_config_by_key_unwrap_or( + format!("{}_requires_cvv", merchant_account.get_id()).as_str(), + Some("true".to_string()), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch requires_cvv config")? + .config + != "false"; + + let off_session_payment_flag = matches!( + payment_intent.setup_future_usage, + Some(common_enums::FutureUsage::OffSession) + ); + + let profile_id = core_utils::get_profile_id_from_business_details( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + merchant_account, + payment_intent.profile_id.as_ref(), + db, + false, + ) + .await + .attach_printable("Could not find profile id from business details")?; + + let business_profile = core_utils::validate_and_get_business_profile( + db, + Some(profile_id).as_ref(), + merchant_account.get_id(), + ) + .await?; + + let is_connector_agnostic_mit_enabled = business_profile + .as_ref() + .and_then(|business_profile| business_profile.is_connector_agnostic_mit_enabled) + .unwrap_or(false); + + Ok(Self { + payment_intent, + business_profile, + requires_cvv, + off_session_payment_flag, + is_connector_agnostic_mit_enabled, + }) + } + + pub async fn perform_payment_ops( + &self, + state: &routes::SessionState, + parent_payment_method_token: Option, + pma: &api::CustomerPaymentMethod, + pm_list_context: PaymentMethodListContext, + ) -> errors::RouterResult<()> { + let token = parent_payment_method_token + .as_ref() + .get_required_value("parent_payment_method_token")?; + let hyperswitch_token_data = pm_list_context + .hyperswitch_token_data + .get_required_value("PaymentTokenData")?; + + let intent_fulfillment_time = self + .business_profile + .as_ref() + .and_then(|b_profile| b_profile.intent_fulfillment_time) + .unwrap_or(consts::DEFAULT_INTENT_FULFILLMENT_TIME); + + ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method)) + .insert(intent_fulfillment_time, hyperswitch_token_data, state) + .await?; + + if let Some(metadata) = pma.metadata.clone() { + let pm_metadata_vec: payment_methods::PaymentMethodMetadata = metadata + .parse_value("PaymentMethodMetadata") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to deserialize metadata to PaymentmethodMetadata struct", + )?; + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + for pm_metadata in pm_metadata_vec.payment_method_tokenization { + let key = format!( + "pm_token_{}_{}_{}", + token, pma.payment_method, pm_metadata.0 + ); + + redis_conn + .set_key_with_expiry(&key, pm_metadata.1, intent_fulfillment_time) + .await + .change_context(errors::StorageError::KVError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add data in redis")?; + } + } + + Ok(()) + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn list_customer_payment_method( + state: &routes::SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payment_intent: Option, + customer_id: &id_type::CustomerId, + limit: Option, + is_payment_associated: bool, +) -> errors::RouterResponse { + let db = &*state.store; + let key_manager_state = &(state).into(); + // let key = key_store.key.get_inner().peek(); + + let customer = db + .find_customer_by_customer_id_merchant_id( + key_manager_state, + customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let payments_info = payment_intent + .async_map(|pi| SavedPMLPaymentsInfo::form_payments_info(pi, &merchant_account, db)) + .await + .transpose()?; + + let saved_payment_methods = db + .find_payment_method_by_customer_id_merchant_id_status( + customer_id, + merchant_account.get_id(), + common_enums::PaymentMethodStatus::Active, + limit, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let mut filtered_saved_payment_methods_ctx = Vec::new(); + for pm in saved_payment_methods.into_iter() { + let payment_method = pm.payment_method.get_required_value("payment_method")?; + let parent_payment_method_token = + is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token")); + + let pm_list_context = get_pm_list_context( + state, + &payment_method, + &key_store, + &pm, + parent_payment_method_token.clone(), + is_payment_associated, + ) + .await?; + + if let Some(ctx) = pm_list_context { + filtered_saved_payment_methods_ctx.push((ctx, parent_payment_method_token, pm)); + } + } + + let pm_list_futures = filtered_saved_payment_methods_ctx + .into_iter() + .map(|ctx| { + generate_saved_pm_response( + state, + &key_store, + &merchant_account, + ctx, + &customer, + payments_info.as_ref(), + ) + }) + .collect::>(); + + let final_result = futures::future::join_all(pm_list_futures).await; + + let mut customer_pms = Vec::new(); + for result in final_result.into_iter() { + let pma = result.attach_printable("saved pm list failed")?; + customer_pms.push(pma); + } + + let mut response = api::CustomerPaymentMethodsListResponse { + customer_payment_methods: customer_pms, + is_guest_customer: is_payment_associated.then_some(false), //to return this key only when the request is tied to a payment intent + }; + + if is_payment_associated { + Box::pin(perform_surcharge_ops( + payments_info.as_ref().map(|pi| pi.payment_intent.clone()), + state, + merchant_account, + key_store, + payments_info.and_then(|pi| pi.business_profile), + &mut response, + )) + .await?; + } + Ok(services::ApplicationResponse::Json(response)) } + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +async fn generate_saved_pm_response( + state: &routes::SessionState, + key_store: &domain::MerchantKeyStore, + merchant_account: &domain::MerchantAccount, + pm_list_context: ( + PaymentMethodListContext, + Option, + diesel_models::PaymentMethod, + ), + customer: &domain::Customer, + payment_info: Option<&SavedPMLPaymentsInfo>, +) -> Result> { + let (pm_list_context, parent_payment_method_token, pm) = pm_list_context; + let payment_method = pm.payment_method.get_required_value("payment_method")?; + + let bank_details = if payment_method == enums::PaymentMethod::BankDebit { + get_masked_bank_details(state, &pm, key_store) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) + } else { + None + }; + + let payment_method_billing = decrypt_generic_data::( + state, + pm.payment_method_billing_address, + key_store, + ) + .await + .attach_printable("unable to decrypt payment method billing address details")?; + + let connector_mandate_details = pm + .connector_mandate_details + .clone() + .map(|val| val.parse_value::("PaymentsMandateReference")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; + + let (is_connector_agnostic_mit_enabled, requires_cvv, off_session_payment_flag) = payment_info + .map(|pi| { + ( + pi.is_connector_agnostic_mit_enabled, + pi.requires_cvv, + pi.off_session_payment_flag, + ) + }) + .unwrap_or((false, false, false)); + + let mca_enabled = get_mca_status( + state, + key_store, + merchant_account.get_id(), + is_connector_agnostic_mit_enabled, + connector_mandate_details, + pm.network_transaction_id.as_ref(), + ) + .await?; + + let requires_cvv = if is_connector_agnostic_mit_enabled { + requires_cvv + && !(off_session_payment_flag + && (pm.connector_mandate_details.is_some() || pm.network_transaction_id.is_some())) + } else { + requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()) + }; + + let pmd = if let Some(card) = pm_list_context.card_details.as_ref() { + Some(api::PaymentMethodListData::Card(card.clone())) + } else if cfg!(feature = "payouts") { + pm_list_context + .bank_transfer_details + .clone() + .map(api::PaymentMethodListData::Bank) + } else { + None + }; + + let pma = api::CustomerPaymentMethod { + payment_token: parent_payment_method_token.clone(), + payment_method_id: pm.payment_method_id.clone(), + customer_id: pm.customer_id, + payment_method, + payment_method_type: pm.payment_method_type, + payment_method_issuer: pm.payment_method_issuer, + payment_method_data: pmd, + metadata: pm.metadata, + payment_method_issuer_code: pm.payment_method_issuer_code, + recurring_enabled: mca_enabled, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + created: Some(pm.created_at), + bank: bank_details, + surcharge_details: None, + requires_cvv: requires_cvv + && !(off_session_payment_flag && pm.connector_mandate_details.is_some()), + last_used_at: Some(pm.last_used_at), + default_payment_method_set: customer.default_payment_method_id.is_some() + && customer.default_payment_method_id == Some(pm.payment_method_id), + billing: payment_method_billing, + }; + + payment_info + .async_map(|pi| { + pi.perform_payment_ops(state, parent_payment_method_token, &pma, pm_list_context) + }) + .await + .transpose()?; + + Ok(pma) +} + pub async fn get_mca_status( state: &routes::SessionState, key_store: &domain::MerchantKeyStore, @@ -4398,7 +4839,7 @@ pub async fn update_last_used_at( pub async fn get_bank_from_hs_locker( state: &routes::SessionState, key_store: &domain::MerchantKeyStore, - temp_token: &str, + temp_token: Option<&String>, customer_id: &id_type::CustomerId, merchant_id: &id_type::MerchantId, token_ref: &str, @@ -4421,16 +4862,18 @@ pub async fn get_bank_from_hs_locker( .change_context(errors::ApiErrorResponse::InternalServerError)?; match &pm_parsed { api::PayoutMethodData::Bank(bank) => { - vault::Vault::store_payout_method_data_in_locker( - state, - Some(temp_token.to_string()), - &pm_parsed, - Some(customer_id.to_owned()), - key_store, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error storing payout method data in temporary locker")?; + if let Some(token) = temp_token { + vault::Vault::store_payout_method_data_in_locker( + state, + Some(token.clone()), + &pm_parsed, + Some(customer_id.to_owned()), + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error storing payout method data in temporary locker")?; + } Ok(bank.to_owned()) } api::PayoutMethodData::Card(_) => Err(errors::ApiErrorResponse::InvalidRequestData { @@ -4694,6 +5137,7 @@ where pub async fn list_countries_currencies_for_connector_payment_method( state: routes::SessionState, req: ListCountriesCurrenciesRequest, + _profile_id: Option, ) -> errors::RouterResponse { Ok(services::ApplicationResponse::Json( list_countries_currencies_for_connector_payment_method_util( diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 6a3ceeada1c5..23f79848dee8 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -3,7 +3,16 @@ use api_models::{ payments, routing, surcharge_decision_configs::{self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord}, }; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] use common_utils::{ext_traits::StringExt, types as common_utils_types}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use common_utils::{ + ext_traits::{OptionExt, StringExt}, + types as common_utils_types, +}; use error_stack::{self, ResultExt}; use euclid::{ backend, @@ -267,6 +276,11 @@ where } Ok(surcharge_metadata) } + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn perform_surcharge_decision_management_for_saved_cards( state: &SessionState, algorithm_ref: routing::RoutingAlgorithmRef, @@ -303,10 +317,13 @@ pub async fn perform_surcharge_decision_management_for_saved_cards( .change_context(ConfigError::InputConstructionError)?; for customer_payment_method in customer_payment_method_list.iter_mut() { + let payment_token = customer_payment_method.payment_token.clone(); + backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); backend_input.payment_method.payment_method_type = customer_payment_method.payment_method_type; - backend_input.payment_method.card_network = customer_payment_method + + let card_network = customer_payment_method .card .as_ref() .and_then(|card| card.scheme.as_ref()) @@ -317,13 +334,98 @@ pub async fn perform_surcharge_decision_management_for_saved_cards( .change_context(ConfigError::DslExecutionError) }) .transpose()?; + + backend_input.payment_method.card_network = card_network; + + let surcharge_details = surcharge_source + .generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + payment_attempt, + ( + &mut surcharge_metadata, + types::SurchargeKey::Token(payment_token), + ), + )?; + customer_payment_method.surcharge_details = surcharge_details + .map(|surcharge_details| { + SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) + .change_context(ConfigError::DslParsingError) + }) + .transpose()?; + } + Ok(surcharge_metadata) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn perform_surcharge_decision_management_for_saved_cards( + state: &SessionState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &storage::PaymentAttempt, + payment_intent: &storage::PaymentIntent, + customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], +) -> ConditionalConfigResult { + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let surcharge_source = match ( + payment_attempt.get_surcharge_details(), + algorithm_ref.surcharge_config_algo_id, + ) { + (Some(request_surcharge_details), _) => { + SurchargeSource::Predetermined(request_surcharge_details) + } + (None, Some(algorithm_id)) => { + let cached_algo = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_id.as_str(), + ) + .await?; + + SurchargeSource::Generate(cached_algo) + } + (None, None) => return Ok(surcharge_metadata), + }; + let surcharge_source_log_message = match &surcharge_source { + SurchargeSource::Generate(_) => "Surcharge was calculated through surcharge rules", + SurchargeSource::Predetermined(_) => "Surcharge was sent in payment create request", + }; + logger::debug!(customer_saved_card_list_surcharge_source = surcharge_source_log_message); + let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) + .change_context(ConfigError::InputConstructionError)?; + + for customer_payment_method in customer_payment_method_list.iter_mut() { + let payment_token = customer_payment_method + .payment_token + .clone() + .get_required_value("payment_token") + .change_context(ConfigError::InputConstructionError)?; + + backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); + backend_input.payment_method.payment_method_type = + customer_payment_method.payment_method_type; + + let card_network = match &customer_payment_method.payment_method_data { + Some(api_models::payment_methods::PaymentMethodListData::Card(card)) => card + .scheme + .as_ref() + .map(|scheme| { + scheme + .clone() + .parse_enum("CardNetwork") + .change_context(ConfigError::DslExecutionError) + }) + .transpose()?, + _ => None, + }; + + backend_input.payment_method.card_network = card_network; + let surcharge_details = surcharge_source .generate_surcharge_details_and_populate_surcharge_metadata( &backend_input, payment_attempt, ( &mut surcharge_metadata, - types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), + types::SurchargeKey::Token(payment_token), ), )?; customer_payment_method.surcharge_details = surcharge_details diff --git a/crates/router/src/core/payment_methods/validator.rs b/crates/router/src/core/payment_methods/validator.rs index c45f6df837d9..d9ec794151c8 100644 --- a/crates/router/src/core/payment_methods/validator.rs +++ b/crates/router/src/core/payment_methods/validator.rs @@ -1,5 +1,5 @@ use api_models::{admin, payment_methods::PaymentMethodCollectLinkRequest}; -use common_utils::{ext_traits::ValueExt, link_utils}; +use common_utils::link_utils; use diesel_models::generic_link::PaymentMethodCollectLinkData; use error_stack::ResultExt; use masking::Secret; @@ -62,18 +62,28 @@ pub async fn validate_request_and_initiate_payment_method_collect_link( // Fetch all configs let default_config = &state.conf.generic_link.payment_method_collect; + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] let merchant_config = merchant_account .pm_collect_link_config .as_ref() .map(|config| { - config - .clone() - .parse_value::("BusinessCollectLinkConfig") + common_utils::ext_traits::ValueExt::parse_value::( + config.clone(), + "BusinessCollectLinkConfig", + ) }) .transpose() .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "pm_collect_link_config in merchant_account", })?; + + #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + let merchant_config = Option::::None; + let merchant_ui_config = merchant_config.as_ref().map(|c| c.config.ui_config.clone()); let ui_config = req .ui_config diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 79cbeccdf2e1..34a5c337ce43 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1967,16 +1967,14 @@ where { connector_label } else { - let profile_id = utils::get_profile_id_from_business_details( - payment_data.payment_intent.business_country, - payment_data.payment_intent.business_label.as_ref(), - merchant_account, - payment_data.payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .attach_printable("Could not find profile id from business details")?; + let profile_id = payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); format!("{connector_name}_{profile_id}") }; @@ -2243,22 +2241,19 @@ pub async fn construct_profile_id_and_get_mca<'a, F>( connector_name: &str, merchant_connector_id: Option<&String>, key_store: &domain::MerchantKeyStore, - should_validate: bool, + _should_validate: bool, ) -> RouterResult where F: Clone, { - let profile_id = utils::get_profile_id_from_business_details( - payment_data.payment_intent.business_country, - payment_data.payment_intent.business_label.as_ref(), - merchant_account, - payment_data.payment_intent.profile_id.as_ref(), - &*state.store, - should_validate, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; + let profile_id = payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index f5abd22480bc..72264c4eff76 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -230,8 +230,8 @@ default_imp_for_complete_authorize!( connector::Trustpay, connector::Tsys, connector::Volt, - connector::Wellsfargo, connector::Wise, + connector::Wellsfargo, connector::Worldline, connector::Worldpay, connector::Zen, @@ -2792,7 +2792,6 @@ default_imp_for_incremental_authorization!( connector::Trustpay, connector::Tsys, connector::Volt, - connector::Wellsfargo, connector::Wise, connector::Worldline, connector::Worldpay, @@ -2882,7 +2881,6 @@ default_imp_for_revoking_mandates!( connector::Trustpay, connector::Tsys, connector::Volt, - connector::Wellsfargo, connector::Wise, connector::Worldline, connector::Worldpay, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 53fc1e1938c5..9f661a8b0c5f 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2942,6 +2942,10 @@ pub async fn verify_payment_intent_time_and_client_secret( .transpose() } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] /// Check whether the business details are configured in the merchant account pub fn validate_business_details( business_country: Option, @@ -2974,41 +2978,6 @@ pub fn validate_business_details( Ok(()) } -/// Do lazy parsing of primary business details -/// If both country and label are passed, no need to parse business details from merchant_account -/// If any one is missing, get it from merchant_account -/// If there is more than one label or country configured in merchant account, then -/// passing business details for payment is mandatory to avoid ambiguity -pub fn get_business_details( - business_country: Option, - business_label: Option<&String>, - merchant_account: &domain::MerchantAccount, -) -> RouterResult<(api_enums::CountryAlpha2, String)> { - let primary_business_details = merchant_account - .primary_business_details - .clone() - .parse_value::>("PrimaryBusinessDetails") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to parse primary business details")?; - - match business_country.zip(business_label) { - Some((business_country, business_label)) => { - Ok((business_country.to_owned(), business_label.to_owned())) - } - _ => match primary_business_details.first() { - Some(business_details) if primary_business_details.len() == 1 => Ok(( - business_country.unwrap_or_else(|| business_details.country.to_owned()), - business_label - .map(ToString::to_string) - .unwrap_or_else(|| business_details.business.to_owned()), - )), - _ => Err(report!(errors::ApiErrorResponse::MissingRequiredField { - field_name: "business_country, business_label" - })), - }, - } -} - #[inline] pub(crate) fn get_payment_id_from_client_secret(cs: &str) -> RouterResult { let (payment_id, _) = cs diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 1723084d0f19..94eddff25d4e 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -80,6 +80,10 @@ impl GetTracker, api::PaymentsRequest> for Pa .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] helpers::validate_business_details( request.business_country, request.business_label.as_ref(), @@ -87,6 +91,10 @@ impl GetTracker, api::PaymentsRequest> for Pa )?; // If profile id is not passed, get it from the business_country and business_label + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] let profile_id = core_utils::get_profile_id_from_business_details( request.business_country, request.business_label.as_ref(), @@ -97,6 +105,15 @@ impl GetTracker, api::PaymentsRequest> for Pa ) .await?; + // Profile id will be mandatory in v2 in the request / headers + #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + let profile_id = request + .profile_id + .clone() + .get_required_value("profile_id") + .attach_printable("Profile id is a mandatory parameter")?; + + // TODO: eliminate a redundant db call to fetch the business profile // Validate whether profile_id passed in request is valid and is linked to the merchant let business_profile = if let Some(business_profile) = core_utils::validate_and_get_business_profile(db, Some(&profile_id), merchant_id) diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 5e384021e54a..f061fc2f07b8 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -368,16 +368,12 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Database error when querying for merchant connector accounts")?; - let profile_id = crate::core::utils::get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .attach_printable("Could not find profile id from business details")?; + let profile_id = payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")?; let filtered_connector_accounts = helpers::filter_mca_based_on_business_profile(all_connector_accounts, Some(profile_id)); diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index f9a2939eb08c..8dbdc73cea41 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -118,7 +118,10 @@ pub async fn validate_create_request( None => None, }; - // Profile ID + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] let profile_id = core_utils::get_profile_id_from_business_details( req.business_country, req.business_label.as_ref(), @@ -129,6 +132,16 @@ pub async fn validate_create_request( ) .await?; + #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + // Profile id will be mandatory in v2 in the request / headers + let profile_id = req + .profile_id + .clone() + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "profile_id", + }) + .attach_printable("Profile id is a mandatory parameter")?; + Ok((payout_id, payout_method_data, profile_id)) } diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index f550c7e501fe..6444f6bc4570 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -119,6 +119,10 @@ pub async fn update_merchant_routing_dictionary( /// This will help make one of all configured algorithms to be in active state for a particular /// merchant +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] pub async fn update_merchant_active_algorithm_ref( state: &SessionState, key_store: &domain::MerchantKeyStore, @@ -152,6 +156,7 @@ pub async fn update_merchant_active_algorithm_ref( payment_link_config: None, pm_collect_link_config: None, }; + let db = &*state.store; db.update_specific_fields_in_merchant( &state.into(), @@ -170,7 +175,19 @@ pub async fn update_merchant_active_algorithm_ref( Ok(()) } -// TODO: Move it to business_profile + +#[cfg(all(any(feature = "v1", feature = "v2"), feature = "merchant_account_v2"))] +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] +pub async fn update_merchant_active_algorithm_ref( + _state: &SessionState, + _key_store: &domain::MerchantKeyStore, + _config_key: cache::CacheKind<'_>, + _algorithm_id: routing_types::RoutingAlgorithmRef, +) -> RouterResult<()> { + // TODO: handle updating the active routing algorithm for v2 in merchant account + Ok(()) +} + pub async fn update_business_profile_active_algorithm_ref( db: &dyn StorageInterface, current_business_profile: BusinessProfile, diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index f25e13161aa1..1500721cc7bc 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -229,24 +229,19 @@ pub async fn construct_refund_router_data<'a, F>( creds_identifier: Option, charges: Option, ) -> RouterResult> { - let profile_id = get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")?; let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.get_id(), creds_identifier, key_store, - &profile_id, + profile_id, connector_id, payment_attempt.merchant_connector_id.as_ref(), ) @@ -524,17 +519,13 @@ pub async fn construct_accept_dispute_router_data<'a>( key_store: &domain::MerchantKeyStore, dispute: &storage::Dispute, ) -> RouterResult { - let profile_id = get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, @@ -623,17 +614,13 @@ pub async fn construct_submit_evidence_router_data<'a>( submit_evidence_request_data: types::SubmitEvidenceRequestData, ) -> RouterResult { let connector_id = &dispute.connector; - let profile_id = get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, @@ -720,17 +707,13 @@ pub async fn construct_upload_file_router_data<'a>( connector_id: &str, file_key: String, ) -> RouterResult { - let profile_id = get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, @@ -821,17 +804,13 @@ pub async fn construct_defend_dispute_router_data<'a>( ) -> RouterResult { let _db = &*state.store; let connector_id = &dispute.connector; - let profile_id = get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); let merchant_connector_account = helpers::get_merchant_connector_account( state, @@ -1090,6 +1069,10 @@ pub fn get_connector_label( }) } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] /// If profile_id is not passed, use default profile if available, or /// If business_details (business_country and business_label) are passed, get the business_profile /// or return a `MissingRequiredField` error diff --git a/crates/router/src/core/verification.rs b/crates/router/src/core/verification.rs index c5bb43f12c85..9294b8f83e96 100644 --- a/crates/router/src/core/verification.rs +++ b/crates/router/src/core/verification.rs @@ -12,6 +12,7 @@ pub async fn verify_merchant_creds_for_applepay( state: SessionState, body: verifications::ApplepayMerchantVerificationRequest, merchant_id: common_utils::id_type::MerchantId, + _profile_id: Option, ) -> CustomResult, errors::ApiErrorResponse> { let applepay_merchant_configs = state.conf.applepay_merchant_configs.get_inner(); diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 2e43d4698153..5a828ebb0cdc 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -580,6 +580,10 @@ async fn publish_and_redact_merchant_account_cache( .as_ref() .map(|publishable_key| CacheKind::Accounts(publishable_key.into())); + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] let cgraph_key = merchant_account.default_profile.as_ref().map(|profile_id| { CacheKind::CGraph( format!( @@ -591,6 +595,10 @@ async fn publish_and_redact_merchant_account_cache( ) }); + // TODO: we will not have default profile in v2 + #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + let cgraph_key = None; + let mut cache_keys = vec![CacheKind::Accounts( merchant_account.get_id().get_string_repr().into(), )]; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0b42c71944eb..ae31db29322f 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -30,7 +30,7 @@ use super::blocklist; use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; -#[cfg(all(any(feature = "olap", feature = "oltp"), not(feature = "customer_v2")))] +#[cfg(any(feature = "olap", feature = "oltp"))] use super::payment_methods::*; #[cfg(feature = "payouts")] use super::payout_link::*; @@ -501,7 +501,30 @@ impl DummyConnector { pub struct Payments; -#[cfg(any(feature = "olap", feature = "oltp"))] +#[cfg(all( + any(feature = "olap", feature = "oltp"), + feature = "v2", + feature = "payment_methods_v2", + feature = "payment_v2" +))] +impl Payments { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/v2/payments").app_data(web::Data::new(state)); + route = route.service( + web::resource("/{payment_id}/saved_payment_methods") + .route(web::get().to(list_customer_payment_method_for_payment)), + ); + + route + } +} + +#[cfg(all( + any(feature = "olap", feature = "oltp"), + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2"), + not(feature = "payment_v2") +))] impl Payments { pub fn server(state: AppState) -> Scope { let mut route = web::scope("/payments").app_data(web::Data::new(state)); @@ -590,7 +613,7 @@ impl Payments { ) .service( web::resource("/{payment_id}/extended_card_info").route(web::get().to(retrieve_extended_card_info)), - ); + ) } route } @@ -844,6 +867,7 @@ pub struct Customers; #[cfg(all( feature = "v2", feature = "customer_v2", + feature = "payment_methods_v2", any(feature = "olap", feature = "oltp") ))] impl Customers { @@ -853,6 +877,13 @@ impl Customers { { route = route.service(web::resource("").route(web::post().to(customers_create))) } + #[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2"))] + { + route = route.service( + web::resource("/{customer_id}/saved_payment_methods") + .route(web::get().to(list_customer_payment_method_api)), + ); + } route } } @@ -860,6 +891,7 @@ impl Customers { #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "customer_v2"), + not(feature = "payment_methods_v2"), any(feature = "olap", feature = "oltp") ))] impl Customers { @@ -897,13 +929,12 @@ impl Customers { .route(web::get().to(customers_retrieve)) .route(web::post().to(customers_update)) .route(web::delete().to(customers_delete)), - ); + ) } route } } - pub struct Refunds; #[cfg(any(feature = "olap", feature = "oltp"))] @@ -1097,6 +1128,11 @@ impl MerchantAccount { web::scope("/v2/accounts") .app_data(web::Data::new(state)) .service(web::resource("").route(web::post().to(merchant_account_create))) + .service( + web::resource("/{id}") + .route(web::get().to(retrieve_merchant_account)) + .route(web::post().to(update_merchant_account)), + ) } } diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index 182f5cc2373c..bb72112c8899 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -64,7 +64,13 @@ pub async fn customers_retrieve( &req, payload, |state, auth, req, _| { - retrieve_customer(state, auth.merchant_account, None, auth.key_store, req) + retrieve_customer( + state, + auth.merchant_account, + auth.profile_id, + auth.key_store, + req, + ) }, &*auth, api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index f55f6c80e7ab..7b2c406c7749 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -229,6 +229,11 @@ pub async fn list_payment_method_api( )) .await } + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] /// List payment methods for a Customer /// /// To filter and list the applicable payment methods for a particular Customer ID @@ -287,6 +292,134 @@ pub async fn list_customer_payment_method_api( )) .await } + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +/// List payment methods for a Customer v2 +/// +/// To filter and list the applicable payment methods for a particular Customer ID, is to be associated with a payment +#[utoipa::path( + get, + path = "v2/payments/{payment_id}/saved_payment_methods", + params ( + ("client-secret" = String, Path, description = "A secret known only to your application and the authorization server"), + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("publishable_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))] +pub async fn list_customer_payment_method_for_payment( + state: web::Data, + payment_id: web::Path<(String,)>, + req: HttpRequest, + query_payload: web::Query, +) -> HttpResponse { + let flow = Flow::CustomerPaymentMethodsList; + let payload = query_payload.into_inner(); + let _payment_id = payment_id.into_inner().0.clone(); + + let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return api::log_and_return_error_response(e), + }; + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req, _| { + cards::list_customer_payment_method_util( + state, + auth.merchant_account, + auth.key_store, + Some(req), + None, + true, + ) + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +/// List payment methods for a Customer v2 +/// +/// To filter and list the applicable payment methods for a particular Customer ID, to be used in a non-payments context +#[utoipa::path( + get, + path = "v2/customers/{customer_id}/saved_payment_methods", + params ( + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))] +pub async fn list_customer_payment_method_api( + state: web::Data, + customer_id: web::Path<(id_type::CustomerId,)>, + req: HttpRequest, + query_payload: web::Query, +) -> HttpResponse { + let flow = Flow::CustomerPaymentMethodsList; + let payload = query_payload.into_inner(); + let customer_id = customer_id.into_inner().0.clone(); + + let ephemeral_or_api_auth = match auth::is_ephemeral_auth(req.headers()) { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(err), + }; + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req, _| { + cards::list_customer_payment_method_util( + state, + auth.merchant_account, + auth.key_store, + Some(req), + Some(customer_id.clone()), + false, + ) + }, + &*ephemeral_or_api_auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] /// List payment methods for a Customer /// /// To filter and list the applicable payment methods for a particular Customer ID @@ -510,8 +643,12 @@ pub async fn list_countries_currencies_for_connector_payment_method( state, &req, payload, - |state, _auth: auth::AuthenticationData, req, _| { - cards::list_countries_currencies_for_connector_payment_method(state, req) + |state, auth: auth::AuthenticationData, req, _| { + cards::list_countries_currencies_for_connector_payment_method( + state, + req, + auth.profile_id, + ) }, #[cfg(not(feature = "release"))] auth::auth_type( diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 4c5af4acd539..a4db90f28b61 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -130,7 +130,7 @@ pub async fn payments_create( state, req_state, auth.merchant_account, - None, + auth.profile_id, auth.key_store, HeaderPayload::default(), req, @@ -423,7 +423,7 @@ pub async fn payments_update( state, req_state, auth.merchant_account, - None, + auth.profile_id, auth.key_store, HeaderPayload::default(), req, @@ -502,7 +502,7 @@ pub async fn payments_confirm( state, req_state, auth.merchant_account, - None, + auth.profile_id, auth.key_store, header_payload.clone(), req, diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index 41993408791b..d064da0517fb 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -22,11 +22,12 @@ pub async fn apple_pay_merchant_registration( state, &req, json_payload.into_inner(), - |state, _, body, _| { + |state, auth, body, _| { verification::verify_merchant_creds_for_applepay( state.clone(), body, merchant_id.clone(), + auth.profile_id, ) }, auth::auth_type( diff --git a/crates/router/src/routes/verify_connector.rs b/crates/router/src/routes/verify_connector.rs index 74b115e6200f..b510ba29637f 100644 --- a/crates/router/src/routes/verify_connector.rs +++ b/crates/router/src/routes/verify_connector.rs @@ -20,7 +20,9 @@ pub async fn payment_connector_verify( state, &req, json_payload.into_inner(), - |state, _: (), req, _| verify_connector::verify_connector_credentials(state, req, None), + |state, auth: auth::AuthenticationData, req, _| { + verify_connector::verify_connector_credentials(state, req, auth.profile_id) + }, &auth::JWTAuth(Permission::MerchantConnectorAccountWrite), api_locking::LockAction::NotApplicable, )) diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 550b1c7af140..ccc9639852c2 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -337,7 +337,7 @@ impl ConnectorData { } enums::Connector::Boku => Ok(ConnectorEnum::Old(Box::new(&connector::Boku))), enums::Connector::Braintree => { - Ok(ConnectorEnum::Old(Box::new(&connector::Braintree))) + Ok(ConnectorEnum::Old(Box::new(connector::Braintree::new()))) } enums::Connector::Cashtocode => { Ok(ConnectorEnum::Old(Box::new(connector::Cashtocode::new()))) @@ -468,9 +468,9 @@ impl ConnectorData { } enums::Connector::Tsys => Ok(ConnectorEnum::Old(Box::new(&connector::Tsys))), enums::Connector::Volt => Ok(ConnectorEnum::Old(Box::new(connector::Volt::new()))), - // enums::Connector::Wellsfargo => { - // Ok(ConnectorEnum::Old(Box::new(connector::Wellsfargo::new()))) - // } + enums::Connector::Wellsfargo => { + Ok(ConnectorEnum::Old(Box::new(&connector::Wellsfargo))) + } enums::Connector::Zen => Ok(ConnectorEnum::Old(Box::new(&connector::Zen))), enums::Connector::Zsl => Ok(ConnectorEnum::Old(Box::new(&connector::Zsl))), enums::Connector::Plaid => { diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 90b29f13b4b3..e7127e5d53bc 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -11,10 +11,7 @@ pub use api_models::{ }, organization::{OrganizationId, OrganizationRequest, OrganizationResponse}, }; -use common_utils::{ - ext_traits::{AsyncExt, ValueExt}, - types::keymanager::Identifier, -}; +use common_utils::{ext_traits::ValueExt, types::keymanager::Identifier}; use diesel_models::organization::OrganizationBridge; use error_stack::ResultExt; use hyperswitch_domain_models::{ @@ -23,7 +20,7 @@ use hyperswitch_domain_models::{ use masking::{ExposeInterface, PeekInterface}; use crate::{ - core::{errors, payment_methods::cards::create_encrypted_data}, + core::errors, routes::SessionState, types::{ domain, storage, @@ -110,7 +107,6 @@ impl ForeignTryFrom for MerchantAccountResponse { publishable_key: item.publishable_key, metadata: item.metadata, organization_id: item.organization_id, - is_recon_enabled: item.is_recon_enabled, recon_status: item.recon_status, }) } @@ -177,7 +173,23 @@ pub async fn business_profile_response( }) } -#[cfg(any(feature = "v1", feature = "v2"))] +#[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + +pub async fn create_business_profile( + _state: &SessionState, + _request: BusinessProfileCreate, + _key_store: &MerchantKeyStore, +) -> Result< + storage::business_profile::BusinessProfileNew, + error_stack::Report, +> { + todo!() +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") +))] pub async fn create_business_profile( state: &SessionState, merchant_account: domain::MerchantAccount, @@ -187,6 +199,10 @@ pub async fn create_business_profile( storage::business_profile::BusinessProfileNew, error_stack::Report, > { + use common_utils::ext_traits::AsyncExt; + + use crate::core; + // Generate a unique profile id let profile_id = common_utils::generate_id_with_default_len("pro"); let merchant_id = merchant_account.get_id().to_owned(); @@ -203,13 +219,25 @@ pub async fn create_business_profile( let payment_link_config_value = request.payment_link_config.map(ForeignInto::foreign_into); let outgoing_webhook_custom_http_headers = request .outgoing_webhook_custom_http_headers - .async_map(|headers| create_encrypted_data(state, key_store, headers)) + .async_map(|headers| { + core::payment_methods::cards::create_encrypted_data(state, key_store, headers) + }) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt outgoing webhook custom HTTP headers")?; - let payout_link_config = request.payout_link_config.map(ForeignInto::foreign_into); + let payout_link_config = request + .payout_link_config + .map(|payout_conf| match payout_conf.config.validate() { + Ok(_) => Ok(payout_conf.foreign_into()), + Err(e) => Err(error_stack::report!( + errors::ApiErrorResponse::InvalidRequestData { + message: e.to_string() + } + )), + }) + .transpose()?; Ok(storage::business_profile::BusinessProfileNew { profile_id, diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 77873503d6e4..062a2fdf6307 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,3 +1,19 @@ +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub use api_models::payment_methods::{ + CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, + CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest, + GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, + PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, + PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, + PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse, + PaymentMethodMigrate, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, + TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, + TokenizedWalletValue1, TokenizedWalletValue2, +}; +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest, diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 8a8f89f90171..05e3e566c07c 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -81,7 +81,7 @@ impl PaymentTokenData { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentMethodListContext { pub card_details: Option, - pub hyperswitch_token_data: PaymentTokenData, + pub hyperswitch_token_data: Option, #[cfg(feature = "payouts")] pub bank_transfer_details: Option, } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 378028b2acdc..da6f35305b6d 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -298,7 +298,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Trustpay => Self::Trustpay, api_enums::Connector::Tsys => Self::Tsys, api_enums::Connector::Volt => Self::Volt, - // api_enums::Connector::Wellsfargo => Self::Wellsfargo, + api_enums::Connector::Wellsfargo => Self::Wellsfargo, api_enums::Connector::Wise => Self::Wise, api_enums::Connector::Worldline => Self::Worldline, api_enums::Connector::Worldpay => Self::Worldpay, diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 1e8761d1375e..6fad1e2195dc 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -51,7 +51,7 @@ use crate::{ core::{ authentication::types::ExternalThreeDSConnectorMetadata, errors::{self, CustomResult, RouterResult, StorageErrorExt}, - utils, webhooks as webhooks_core, + webhooks as webhooks_core, }, logger, routes::{metrics, SessionState}, @@ -441,20 +441,14 @@ pub async fn get_mca_from_payment_intent( } } None => { - let profile_id = match payment_intent.profile_id { - Some(profile_id) => profile_id, - None => utils::get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - db, - false, - ) - .await + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?, - }; + .attach_printable("profile_id is not set in payment_intent")? + .clone(); + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "merchant_connector_account_v2") @@ -579,7 +573,17 @@ pub async fn get_mca_from_object_reference_id( key_store: &domain::MerchantKeyStore, ) -> CustomResult { let db = &*state.store; - match merchant_account.default_profile.as_ref() { + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] + let default_profile_id = merchant_account.default_profile.as_ref(); + + #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + let default_profile_id = Option::<&String>::None; + + match default_profile_id { Some(profile_id) => { #[cfg(all( any(feature = "v1", feature = "v2"), @@ -601,8 +605,8 @@ pub async fn get_mca_from_object_reference_id( } #[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))] { - let _ = db; - let _ = profile_id; + let _db = db; + let _profile_id = profile_id; todo!() } } diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 86607234d4ee..303f02acc696 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -43,25 +43,44 @@ pub async fn generate_sample_data( .await .change_context::(SampleDataError::DataDoesNotExist)?; - let merchant_parsed_details: Vec = - serde_json::from_value(merchant_from_db.primary_business_details.clone()) - .change_context(SampleDataError::InternalServerError) - .attach_printable("Error while parsing primary business details")?; - - let business_country_default = merchant_parsed_details.first().map(|x| x.country); - - let business_label_default = merchant_parsed_details.first().map(|x| x.business.clone()); - - let profile_id = match crate::core::utils::get_profile_id_from_business_details( - business_country_default, - business_label_default.as_ref(), - &merchant_from_db, - req.profile_id.as_ref(), - &*state.store, - false, - ) - .await - { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "merchant_account_v2") + ))] + let (profile_id_result, business_country_default, business_label_default) = { + let merchant_parsed_details: Vec = + serde_json::from_value(merchant_from_db.primary_business_details.clone()) + .change_context(SampleDataError::InternalServerError) + .attach_printable("Error while parsing primary business details")?; + + let business_country_default = merchant_parsed_details.first().map(|x| x.country); + + let business_label_default = merchant_parsed_details.first().map(|x| x.business.clone()); + + let profile_id = crate::core::utils::get_profile_id_from_business_details( + business_country_default, + business_label_default.as_ref(), + &merchant_from_db, + req.profile_id.as_ref(), + &*state.store, + false, + ) + .await; + (profile_id, business_country_default, business_label_default) + }; + + #[cfg(all(feature = "v2", feature = "merchant_account_v2"))] + let (profile_id_result, business_country_default, business_label_default) = { + let profile_id = req + .profile_id.clone() + .ok_or(hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse::MissingRequiredField { + field_name: "profile_id", + }); + + (profile_id, None, None) + }; + + let profile_id = match profile_id_result { Ok(id) => id.clone(), Err(error) => { router_env::logger::error!( diff --git a/crates/router/tests/connectors/wellsfargo.rs b/crates/router/tests/connectors/wellsfargo.rs index 300a5fbb379f..4425c0773772 100644 --- a/crates/router/tests/connectors/wellsfargo.rs +++ b/crates/router/tests/connectors/wellsfargo.rs @@ -11,8 +11,8 @@ impl utils::Connector for WellsfargoTest { fn get_data(&self) -> api::ConnectorData { use router::connector::Wellsfargo; utils::construct_connector_data_old( - Box::new(Wellsfargo::new()), - types::Connector::Plaid, + Box::new(&Wellsfargo), + types::Connector::Wellsfargo, api::GetToken::Connector, None, ) diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js index 68c8fa61fc4d..c405a39913ea 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js @@ -15,6 +15,7 @@ function normalise(input) { bankofamerica: "Bank of America", cybersource: "Cybersource", paypal: "Paypal", + wellsfargo: "Wellsfargo" // Add more known exceptions here }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index b22f5de535e7..b0a0455cc665 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -13,6 +13,7 @@ import { connectorDetails as nmiConnectorDetails } from "./Nmi.js"; import { connectorDetails as paypalConnectorDetails } from "./Paypal.js"; import { connectorDetails as stripeConnectorDetails } from "./Stripe.js"; import { connectorDetails as trustpayConnectorDetails } from "./Trustpay.js"; +import { connectorDetails as wellsfargoConnectorDetails } from "./WellsFargo.js"; const connectorDetails = { adyen: adyenConnectorDetails, @@ -27,6 +28,7 @@ const connectorDetails = { stripe: stripeConnectorDetails, trustpay: trustpayConnectorDetails, datatrans: datatransConnectorDetails, + wellsfargo: wellsfargoConnectorDetails }; export default function getConnectorDetails(connectorId) { diff --git a/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js b/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js new file mode 100644 index 000000000000..d4bffec83cf7 --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js @@ -0,0 +1,528 @@ +const successfulNo3DSCardDetails = { + card_number: "4242424242424242", + card_exp_month: "01", + card_exp_year: "25", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; + +const successfulThreeDSTestCardDetails = { + card_number: "4000000000001091", + card_exp_month: "01", + card_exp_year: "25", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; + +const singleUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + single_use: { + amount: 8000, + currency: "USD", + }, + }, +}; + +const multiUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + multi_use: { + amount: 8000, + currency: "USD", + }, + }, +}; + +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + "3DSManualCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + trigger_skip: true, + body: { + status: "requires_capture", + }, + }, + }, + "3DSAutoCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + trigger_skip: true, + + body: { + status: "requires_customer_action", + }, + }, + }, + No3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + No3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + Capture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6500, + amount_capturable: 0, + amount_received: 6500, + }, + }, + }, + PartialCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6500, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + Void: { + Request: {}, + Response: { + status: 200, + body: { + status: "cancelled", + }, + }, + }, + Refund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "pending", + }, + }, + }, + PartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "pending", + }, + }, + }, + SyncRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "pending", + }, + }, + }, + MandateSingleUse3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + trigger_skip: true, + + body: { + status: "succeeded", + }, + }, + }, + MandateSingleUse3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + trigger_skip: true, + + body: { + status: "requires_customer_action", + }, + }, + }, + MandateSingleUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateSingleUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MandateMultiUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MandateMultiUse3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + trigger_skip: true, + + body: { + status: "requires_capture", + }, + }, + }, + MandateMultiUse3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + trigger_skip: true, + + body: { + status: "requires_capture", + }, + }, + }, + ZeroAuthMandate: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + PaymentMethodIdMandateNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: null, + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: null, + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + PaymentMethodIdMandate3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "USD", + mandate_data: null, + authentication_type: "three_ds", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + trigger_skip: true, + body: { + status: "requires_customer_action", + }, + }, + }, + PaymentMethodIdMandate3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + mandate_data: null, + authentication_type: "three_ds", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PayoutUtils/Commons.js b/cypress-tests/cypress/e2e/PayoutUtils/Commons.js index 4036ee7f6128..8eee62493b13 100644 --- a/cypress-tests/cypress/e2e/PayoutUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PayoutUtils/Commons.js @@ -14,6 +14,7 @@ function normalise(input) { const exceptions = { adyenplatform: "Adyenplatform", wise: "Wise", + wellsfargo: "Wellsfargo" // Add more known exceptions here }; diff --git a/cypress-tests/cypress/support/redirectionHandler.js b/cypress-tests/cypress/support/redirectionHandler.js index f0998a44fc32..8032c4274aa5 100644 --- a/cypress-tests/cypress/support/redirectionHandler.js +++ b/cypress-tests/cypress/support/redirectionHandler.js @@ -295,7 +295,7 @@ function threeDsRedirection(redirection_url, expected_url, connectorId) { cy.get('input[type="password"]').type("password"); cy.get("#buttonSubmit").click(); }); - } else if (connectorId === "bankofamerica" || connectorId === "cybersource") { + } else if (connectorId === "bankofamerica" || connectorId === "cybersource" || connectorId === "wellsfargo"){ cy.get("iframe", { timeout: TIMEOUT }) .its("0.contentDocument.body") .within((body) => { diff --git a/justfile b/justfile index 1823c1ec19e2..452de7b3012d 100644 --- a/justfile +++ b/justfile @@ -55,12 +55,12 @@ check_v2 *FLAGS: jq -r ' [ ( .workspace_members | sort ) as $package_ids # Store workspace crate package IDs in `package_ids` array | .packages[] | select( IN(.id; $package_ids[]) ) | .features | keys[] ] | unique # Select all unique features from all workspace crates - | del( .[] | select( any( . ; . == ("v1", "merchant_account_v2", "payment_v2","routing_v2") ) ) ) # Exclude some features from features list + | del( .[] | select( any( . ; . == ("v1") ) ) ) # Exclude some features from features list | join(",") # Construct a comma-separated string of features for passing to `cargo` ')" set -x - cargo clippy {{ check_flags }} --features "${FEATURES}" {{ FLAGS }} + cargo check {{ check_flags }} --features "${FEATURES}" {{ FLAGS }} set +x check *FLAGS: @@ -76,7 +76,7 @@ check *FLAGS: ')" set -x - cargo clippy {{ check_flags }} --features "${FEATURES}" {{ FLAGS }} + cargo check {{ check_flags }} --features "${FEATURES}" {{ FLAGS }} set +x alias cl := clippy @@ -120,10 +120,8 @@ euclid-wasm features='dummy_connector': precommit: fmt clippy # Check compilation of v2 feature on base dependencies -v2_intermediate_features := "merchant_account_v2,payment_v2,customer_v2,business_profile_v2" hack_v2: - cargo hack clippy --feature-powerset --depth 2 --ignore-unknown-features --at-least-one-of "v2 " --include-features "v2" --include-features {{ v2_intermediate_features }} --package "hyperswitch_domain_models" --package "diesel_models" --package "api_models" - cargo hack clippy --features "v2,payment_v2" -p storage_impl + scripts/ci-checks-v2.sh # Use the env variables if present, or fallback to default values diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 6bae052394a1..3fa9bb01c4b2 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -88,8 +88,7 @@ bitpay.base_url = "https://test.bitpay.com" bluesnap.base_url = "https://sandbox.bluesnap.com/" bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" boku.base_url = "https://country-api4-stage.boku.com" -braintree.base_url = "https://api.sandbox.braintreegateway.com/" -braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" @@ -288,9 +287,6 @@ discord_invite_url = "https://discord.gg/wJZ7DVW8mm" [payouts] payout_eligibility = true -[multiple_api_version_supported_connectors] -supported_connectors = "braintree" - [mandates.supported_payment_methods] pay_later.klarna = {connector_list = "adyen"} wallet.google_pay = {connector_list = "stripe,adyen,bankofamerica"} diff --git a/scripts/ci-checks-v2.sh b/scripts/ci-checks-v2.sh new file mode 100755 index 000000000000..98697387ec57 --- /dev/null +++ b/scripts/ci-checks-v2.sh @@ -0,0 +1,82 @@ +#! /usr/bin/env bash +set -euo pipefail + +crates_to_check=\ +'api_models +diesel_models +hyperswitch_domain_models +storage_impl' + +v2_feature_set='v2,merchant_account_v2,payment_v2,customer_v2,routing_v2,business_profile_v2' + +packages_checked=() +packages_skipped=() + +# List of cargo commands that will be executed +all_commands=() + +# If we are running this on a pull request, then only check for packages that are modified +if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]]; then + # Obtain the pull request number and files modified in the pull request + pull_request_number="$(jq --raw-output '.pull_request.number' "${GITHUB_EVENT_PATH}")" + files_modified="$( + gh api \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + --paginate \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${pull_request_number}/files" \ + --jq '.[].filename' + )" + + while IFS= read -r package_name; do + # A package must be checked if it has been modified + if grep --quiet --extended-regexp "^crates/${package_name}" <<< "${files_modified}"; then + if [[ "${package_name}" == "storage_impl" ]]; then + all_commands+=("cargo hack clippy --features 'v2,payment_v2' -p storage_impl") + else + all_commands+=("cargo hack clippy --feature-powerset --depth 2 --ignore-unknown-features --at-least-one-of 'v2 ' --include-features '${v2_feature_set}' --package '${package_name}'") + fi + printf '::debug::Checking `%s` since it was modified %s\n' "${package_name}" + packages_checked+=("${package_name}") + else + printf '::debug::Skipping `%s` since it was not modified: %s\n' "${package_name}" + packages_skipped+=("${package_name}") + fi + done <<< "${crates_to_check}" + printf '::notice::Packages checked: %s; Packages skipped: %s\n' "${packages_checked[*]}" "${packages_skipped[*]}" + +else + # If we are doing this locally or on merge queue, then check for all the V2 crates + all_commands+=("cargo hack clippy --features 'v2,payment_v2' -p storage_impl") + + common_command="cargo hack clippy --feature-powerset --depth 2 --ignore-unknown-features --at-least-one-of 'v2 ' --include-features '${v2_feature_set}'" + crates_to_include="" + while IFS= read -r crate; do + if [[ "${crate}" != "storage_impl" ]]; then + crates_to_include+="--package '${crate}' " + fi + done <<< "${crates_to_check}" + all_commands+=("${common_command} ${crates_to_include}") +fi + +if ((${#all_commands[@]} == 0)); then + echo "There are no commands to be be executed" + exit 0 +fi + +echo "The list of commands that will be executed:" +printf "%s\n" "${all_commands[@]}" +echo + +# Execute the commands +for command in "${all_commands[@]}"; do + if [[ "${CI:-false}" == "true" && "${GITHUB_ACTIONS:-false}" == "true" ]]; then + printf '::group::Running `%s`\n' "${command}" + fi + + bash -c -x "${command}" + + if [[ "${CI:-false}" == "true" && "${GITHUB_ACTIONS:-false}" == "true" ]]; then + echo '::endgroup::' + fi +done diff --git a/v2_migrations/2024-07-26-065428_remove_deprecated_field_from_merchant_account/down.sql b/v2_migrations/2024-07-26-065428_remove_deprecated_field_from_merchant_account/down.sql new file mode 100644 index 000000000000..60e6c17b08b0 --- /dev/null +++ b/v2_migrations/2024-07-26-065428_remove_deprecated_field_from_merchant_account/down.sql @@ -0,0 +1,46 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_account +ADD COLUMN return_url VARCHAR(255); + +ALTER TABLE merchant_account +ADD COLUMN enable_payment_response_hash BOOLEAN DEFAULT FALSE; + +ALTER TABLE merchant_account +ADD COLUMN payment_response_hash_key VARCHAR(255); + +ALTER TABLE merchant_account +ADD COLUMN redirect_to_merchant_with_http_post BOOLEAN DEFAULT FALSE; + +ALTER TABLE merchant_account +ADD COLUMN sub_merchants_enabled BOOLEAN DEFAULT FALSE; + +ALTER TABLE merchant_account +ADD COLUMN parent_merchant_id VARCHAR(64); + +-- The default value is for temporary purpose only +ALTER TABLE merchant_account +ADD COLUMN primary_business_details JSON NOT NULL DEFAULT '[{"country": "US", "business": "default"}]'; + +ALTER TABLE merchant_account +ALTER COLUMN primary_business_details DROP DEFAULT; + +ALTER TABLE merchant_account +ADD COLUMN locker_id VARCHAR(64); + +ALTER TABLE merchant_account +ADD COLUMN intent_fulfillment_time BIGINT; + +ALTER TABLE merchant_account +ADD COLUMN default_profile VARCHAR(64); + +ALTER TABLE merchant_account +ADD COLUMN payment_link_config JSONB NULL; + +ALTER TABLE merchant_account +ADD COLUMN pm_collect_link_config JSONB NULL; + +ALTER TABLE merchant_account +ADD COLUMN is_recon_enabled BOOLEAN NOT NULL DEFAULT FALSE; + +ALTER TABLE merchant_account +ADD COLUMN webhook_details JSONB NULL; diff --git a/v2_migrations/2024-07-26-065428_remove_deprecated_field_from_merchant_account/up.sql b/v2_migrations/2024-07-26-065428_remove_deprecated_field_from_merchant_account/up.sql new file mode 100644 index 000000000000..e9a349e61306 --- /dev/null +++ b/v2_migrations/2024-07-26-065428_remove_deprecated_field_from_merchant_account/up.sql @@ -0,0 +1,28 @@ +-- Your SQL goes here +ALTER TABLE merchant_account DROP COLUMN return_url; + +ALTER TABLE merchant_account DROP enable_payment_response_hash; + +ALTER TABLE merchant_account DROP payment_response_hash_key; + +ALTER TABLE merchant_account DROP redirect_to_merchant_with_http_post; + +ALTER TABLE merchant_account DROP sub_merchants_enabled; + +ALTER TABLE merchant_account DROP parent_merchant_id; + +ALTER TABLE merchant_account DROP primary_business_details; + +ALTER TABLE merchant_account DROP locker_id; + +ALTER TABLE merchant_account DROP intent_fulfillment_time; + +ALTER TABLE merchant_account DROP default_profile; + +ALTER TABLE merchant_account DROP payment_link_config; + +ALTER TABLE merchant_account DROP pm_collect_link_config; + +ALTER TABLE merchant_account DROP is_recon_enabled; + +ALTER TABLE merchant_account DROP webhook_details;