From ad9f91b37cc39c8fb594b48ac60c5e945a0f561f Mon Sep 17 00:00:00 2001 From: Kiran Kumar <60121719+KiranKBR@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:13:38 +0530 Subject: [PATCH] feat(connector): [Adyen] add dispute flows for adyen connector (#5514) --- config/config.example.toml | 3 +- config/deployments/integration_test.toml | 3 +- config/deployments/production.toml | 3 +- config/deployments/sandbox.toml | 3 +- config/development.toml | 3 +- config/docker_compose.toml | 3 +- .../src/router_request_types.rs | 22 +- .../src/router_response_types/disputes.rs | 6 + crates/hyperswitch_interfaces/src/configs.rs | 17 +- crates/router/src/connector/adyen.rs | 345 +++++++++++++++++- .../src/connector/adyen/transformers.rs | 292 +++++++++++++++ .../router/src/core/disputes/transformers.rs | 174 ++++----- crates/router/src/core/files.rs | 20 +- crates/router/src/core/files/helpers.rs | 26 +- crates/router/src/core/payments/flows.rs | 4 - loadtest/config/development.toml | 3 +- 16 files changed, 801 insertions(+), 126 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index ebadc9edae79..4397d4d3ef6d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -180,7 +180,8 @@ hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" -adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyen.payout_base_url = "https://pal-test.adyen.com/" +adyen.dispute_base_url = "https://ca-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 84652e11b0a4..e0b4ee659762 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -20,7 +20,8 @@ przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_peka [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" -adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyen.payout_base_url = "https://pal-test.adyen.com/" +adyen.dispute_base_url = "https://ca-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index bac5ec206e6e..67d1ced4e56b 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -24,7 +24,8 @@ payout_connector_list = "stripe,wise" [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpayments.com/checkout/" -adyen.secondary_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/" +adyen.payout_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/" +adyen.dispute_base_url = "https://{{merchant_endpoint_prefix}}-ca-live.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-live.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 669daa10162c..14505f5815ec 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -24,7 +24,8 @@ payout_connector_list = "stripe,wise" [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" -adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyen.payout_base_url = "https://pal-test.adyen.com/" +adyen.dispute_base_url = "https://ca-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" diff --git a/config/development.toml b/config/development.toml index 8facb9e803b0..18cf9ca85a0e 100644 --- a/config/development.toml +++ b/config/development.toml @@ -185,7 +185,8 @@ checksum_auth_key = "54455354" aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" -adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyen.payout_base_url = "https://pal-test.adyen.com/" +adyen.dispute_base_url = "https://ca-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 41f9cf6cfafe..cf92e9c02a87 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -110,7 +110,8 @@ hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" -adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyen.payout_base_url = "https://pal-test.adyen.com/" +adyen.dispute_base_url = "https://ca-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 808000e8dc89..ac7bf932fe70 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -667,42 +667,62 @@ pub struct SubmitEvidenceRequestData { pub connector_dispute_id: String, pub access_activity_log: Option, pub billing_address: Option, + //cancellation policy pub cancellation_policy: Option>, + pub cancellation_policy_file_type: Option, pub cancellation_policy_provider_file_id: Option, pub cancellation_policy_disclosure: Option, pub cancellation_rebuttal: Option, + //customer communication pub customer_communication: Option>, + pub customer_communication_file_type: Option, pub customer_communication_provider_file_id: Option, pub customer_email_address: Option, pub customer_name: Option, pub customer_purchase_ip: Option, + //customer signature pub customer_signature: Option>, + pub customer_signature_file_type: Option, pub customer_signature_provider_file_id: Option, + //product description pub product_description: Option, + //receipts pub receipt: Option>, + pub receipt_file_type: Option, pub receipt_provider_file_id: Option, + //refund policy pub refund_policy: Option>, + pub refund_policy_file_type: Option, pub refund_policy_provider_file_id: Option, pub refund_policy_disclosure: Option, pub refund_refusal_explanation: Option, + //service docs pub service_date: Option, pub service_documentation: Option>, + pub service_documentation_file_type: Option, pub service_documentation_provider_file_id: Option, + //shipping details docs pub shipping_address: Option, pub shipping_carrier: Option, pub shipping_date: Option, pub shipping_documentation: Option>, + pub shipping_documentation_file_type: Option, pub shipping_documentation_provider_file_id: Option, pub shipping_tracking_number: Option, + //invoice details pub invoice_showing_distinct_transactions: Option>, + pub invoice_showing_distinct_transactions_file_type: Option, pub invoice_showing_distinct_transactions_provider_file_id: Option, + //subscription details pub recurring_transaction_agreement: Option>, + pub recurring_transaction_agreement_file_type: Option, pub recurring_transaction_agreement_provider_file_id: Option, + //uncategorized details pub uncategorized_file: Option>, + pub uncategorized_file_type: Option, pub uncategorized_file_provider_file_id: Option, pub uncategorized_text: Option, } - #[derive(Clone, Debug)] pub struct RetrieveFileRequestData { pub provider_file_id: String, diff --git a/crates/hyperswitch_domain_models/src/router_response_types/disputes.rs b/crates/hyperswitch_domain_models/src/router_response_types/disputes.rs index 478cedd44052..3eb50e59caa5 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types/disputes.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types/disputes.rs @@ -15,3 +15,9 @@ pub struct DefendDisputeResponse { pub dispute_status: api_models::enums::DisputeStatus, pub connector_status: Option, } + +pub struct FileInfo { + pub file_data: Option>, + pub provider_file_id: Option, + pub file_type: Option, +} diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index 2a30df3b2cbf..19448d6542a9 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -10,11 +10,8 @@ use serde::Deserialize; #[serde(default)] pub struct Connectors { pub aci: ConnectorParams, - #[cfg(feature = "payouts")] - pub adyen: ConnectorParamsWithSecondaryBaseUrl, + pub adyen: AdyenParamsWithThreeBaseUrls, pub adyenplatform: ConnectorParams, - #[cfg(not(feature = "payouts"))] - pub adyen: ConnectorParams, pub airwallex: ConnectorParams, pub applepay: ConnectorParams, pub authorizedotnet: ConnectorParams, @@ -143,6 +140,18 @@ pub struct ConnectorParamsWithFileUploadUrl { pub base_url_file_upload: String, } +/// struct ConnectorParamsWithThreeBaseUrls +#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] +#[serde(default)] +pub struct AdyenParamsWithThreeBaseUrls { + /// base url + pub base_url: String, + /// secondary base url + #[cfg(feature = "payouts")] + pub payout_base_url: String, + /// third base url + pub dispute_base_url: String, +} /// struct ConnectorParamsWithSecondaryBaseUrl #[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] #[serde(default)] diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 3e54364e5996..f0b84fc93c10 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -1,5 +1,4 @@ pub mod transformers; - use api_models::{enums::PaymentMethodType, webhooks::IncomingWebhookEvent}; use base64::Engine; use common_utils::{ @@ -37,7 +36,6 @@ use crate::{ }, utils::{crypto, ByteSliceExt, BytesExt, OptionExt}, }; - const ADYEN_API_VERSION: &str = "v68"; #[derive(Clone)] @@ -52,16 +50,13 @@ impl Adyen { } } } - impl ConnectorCommon for Adyen { fn id(&self) -> &'static str { "adyen" } - fn get_currency_unit(&self) -> api::CurrencyUnit { api::CurrencyUnit::Minor } - fn get_auth_header( &self, auth_type: &types::ConnectorAuthType, @@ -1151,7 +1146,7 @@ impl services::ConnectorIntegration CustomResult { let endpoint = build_env_specific_endpoint( - connectors.adyen.secondary_base_url.as_str(), + connectors.adyen.payout_base_url.as_str(), req.test_mode, &req.connector_meta_data, )?; @@ -1255,7 +1250,7 @@ impl services::ConnectorIntegration CustomResult { let endpoint = build_env_specific_endpoint( - connectors.adyen.secondary_base_url.as_str(), + connectors.adyen.payout_base_url.as_str(), req.test_mode, &req.connector_meta_data, )?; @@ -1471,7 +1466,7 @@ impl services::ConnectorIntegration CustomResult { let endpoint = build_env_specific_endpoint( - connectors.adyen.secondary_base_url.as_str(), + connectors.adyen.payout_base_url.as_str(), req.test_mode, &req.connector_meta_data, )?; @@ -1910,3 +1905,337 @@ impl api::IncomingWebhook for Adyen { }) } } + +impl api::Dispute for Adyen {} +impl api::DefendDispute for Adyen {} +impl api::AcceptDispute for Adyen {} +impl api::SubmitEvidence for Adyen {} + +impl + services::ConnectorIntegration< + api::Accept, + types::AcceptDisputeRequestData, + types::AcceptDisputeResponse, + > for Adyen +{ + fn get_headers( + &self, + req: &types::AcceptDisputeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::AcceptDisputeType::get_content_type(self) + .to_string() + .into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } + + fn get_url( + &self, + req: &types::AcceptDisputeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let endpoint = build_env_specific_endpoint( + connectors.adyen.dispute_base_url.as_str(), + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!( + "{}ca/services/DisputeService/v30/acceptDispute", + endpoint + )) + } + + fn build_request( + &self, + req: &types::AcceptDisputeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::AcceptDisputeType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::AcceptDisputeType::get_headers( + self, req, connectors, + )?) + .set_body(types::AcceptDisputeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn get_request_body( + &self, + req: &types::AcceptDisputeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = adyen::AdyenAcceptDisputeRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn handle_response( + &self, + data: &types::AcceptDisputeRouterData, + _event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult { + let response: adyen::AdyenDisputeResponse = res + .response + .parse_struct("AdyenDisputeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::foreign_try_from((data, response)) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl + services::ConnectorIntegration< + api::Defend, + types::DefendDisputeRequestData, + types::DefendDisputeResponse, + > for Adyen +{ + fn get_headers( + &self, + req: &types::DefendDisputeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::DefendDisputeType::get_content_type(self) + .to_string() + .into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } + + fn get_url( + &self, + req: &types::DefendDisputeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let endpoint = build_env_specific_endpoint( + connectors.adyen.dispute_base_url.as_str(), + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!( + "{}ca/services/DisputeService/v30/defendDispute", + endpoint + )) + } + + fn build_request( + &self, + req: &types::DefendDisputeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::DefendDisputeType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::DefendDisputeType::get_headers( + self, req, connectors, + )?) + .set_body(types::DefendDisputeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn get_request_body( + &self, + req: &types::DefendDisputeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = adyen::AdyenDefendDisputeRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn handle_response( + &self, + data: &types::DefendDisputeRouterData, + _event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult { + let response: adyen::AdyenDisputeResponse = res + .response + .parse_struct("AdyenDisputeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::foreign_try_from((data, response)) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl + services::ConnectorIntegration< + api::Evidence, + types::SubmitEvidenceRequestData, + types::SubmitEvidenceResponse, + > for Adyen +{ + fn get_headers( + &self, + req: &types::SubmitEvidenceRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::SubmitEvidenceType::get_content_type(self) + .to_string() + .into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } + + fn get_url( + &self, + req: &types::SubmitEvidenceRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let endpoint = build_env_specific_endpoint( + connectors.adyen.dispute_base_url.as_str(), + req.test_mode, + &req.connector_meta_data, + )?; + Ok(format!( + "{}ca/services/DisputeService/v30/supplyDefenseDocument", + endpoint + )) + } + + fn get_request_body( + &self, + req: &types::SubmitEvidenceRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = adyen::Evidence::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::SubmitEvidenceRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::SubmitEvidenceType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SubmitEvidenceType::get_headers( + self, req, connectors, + )?) + .set_body(types::SubmitEvidenceType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::SubmitEvidenceRouterData, + _event_builder: Option<&mut ConnectorEvent>, + res: types::Response, + ) -> CustomResult { + let response: adyen::AdyenDisputeResponse = res + .response + .parse_struct("AdyenDisputeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::foreign_try_from((data, response)) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} +impl api::UploadFile for Adyen {} +impl api::RetrieveFile for Adyen {} +impl + services::ConnectorIntegration< + api::Retrieve, + types::RetrieveFileRequestData, + types::RetrieveFileResponse, + > for Adyen +{ +} +impl + services::ConnectorIntegration< + api::Upload, + types::UploadFileRequestData, + types::UploadFileResponse, + > for Adyen +{ +} +#[async_trait::async_trait] +impl api::FileUpload for Adyen { + fn validate_file_upload( + &self, + purpose: api::FilePurpose, + file_size: i32, + file_type: mime::Mime, + ) -> CustomResult<(), errors::ConnectorError> { + match purpose { + api::FilePurpose::DisputeEvidence => { + let supported_file_types = + ["image/jpeg", "image/jpg", "image/png", "application/pdf"]; + if !supported_file_types.contains(&file_type.to_string().as_str()) { + Err(errors::ConnectorError::FileValidationFailed { + reason: "file_type does not match JPEG, JPG, PNG, or PDF format".to_owned(), + })? + } + //10 MB + if (file_type.to_string().as_str() == "image/jpeg" + || file_type.to_string().as_str() == "image/jpg" + || file_type.to_string().as_str() == "image/png") + && file_size > 10000000 + { + Err(errors::ConnectorError::FileValidationFailed { + reason: "file_size exceeded the max file size of 10MB for Image formats" + .to_owned(), + })? + } + //2 MB + if file_type.to_string().as_str() == "application/pdf" && file_size > 2000000 { + Err(errors::ConnectorError::FileValidationFailed { + reason: "file_size exceeded the max file size of 2MB for PDF formats" + .to_owned(), + })? + } + } + } + Ok(()) + } +} diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index b1caf65f398b..a9431d957ea8 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -4,6 +4,7 @@ use api_models::{enums, payments, webhooks}; use cards::CardNumber; use common_utils::{errors::ParsingError, ext_traits::Encode, id_type, pii, types::MinorUnit}; use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::router_request_types::SubmitEvidenceRequestData; use masking::{ExposeInterface, PeekInterface}; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -4860,3 +4861,294 @@ impl From for storage_enums::PayoutStatus { } } } + +fn get_merchant_account_code( + auth_type: &types::ConnectorAuthType, +) -> errors::CustomResult, errors::ConnectorError> { + let auth = AdyenAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(auth.merchant_account.clone()) +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenAcceptDisputeRequest { + dispute_psp_reference: String, + merchant_account_code: Secret, +} + +#[derive(Default, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenDisputeResponse { + pub error_message: Option, + pub success: bool, +} + +impl TryFrom<&types::AcceptDisputeRouterData> for AdyenAcceptDisputeRequest { + type Error = Error; + fn try_from(item: &types::AcceptDisputeRouterData) -> Result { + let merchant_account_code = get_merchant_account_code(&item.connector_auth_type)?; + Ok(Self { + dispute_psp_reference: item.clone().request.connector_dispute_id, + merchant_account_code, + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenDefendDisputeRequest { + dispute_psp_reference: String, + merchant_account_code: Secret, + defense_reason_code: String, +} + +impl TryFrom<&types::DefendDisputeRouterData> for AdyenDefendDisputeRequest { + type Error = Error; + fn try_from(item: &types::DefendDisputeRouterData) -> Result { + let merchant_account_code = get_merchant_account_code(&item.connector_auth_type)?; + Ok(Self { + dispute_psp_reference: item.request.connector_dispute_id.clone(), + merchant_account_code, + defense_reason_code: "SupplyDefenseMaterial".into(), + }) + } +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] + +pub struct Evidence { + defense_documents: Vec, + merchant_account_code: Secret, + dispute_psp_reference: String, +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] + +pub struct DefenseDocuments { + content: Secret, + content_type: Option, + defense_document_type_code: String, +} + +impl TryFrom<&types::SubmitEvidenceRouterData> for Evidence { + type Error = error_stack::Report; + fn try_from(item: &types::SubmitEvidenceRouterData) -> Result { + let merchant_account_code = get_merchant_account_code(&item.connector_auth_type)?; + let submit_evidence_request_data = item.request.clone(); + Ok(Self { + defense_documents: get_defence_documents(submit_evidence_request_data).ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "Missing Defence Documents", + }, + )?, + merchant_account_code, + dispute_psp_reference: item.request.connector_dispute_id.clone(), + }) + } +} + +fn get_defence_documents(item: SubmitEvidenceRequestData) -> Option> { + let mut defense_documents: Vec = Vec::new(); + if let Some(shipping_documentation) = item.shipping_documentation { + defense_documents.push(DefenseDocuments { + content: get_content(shipping_documentation).into(), + content_type: item.receipt_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(receipt) = item.receipt { + defense_documents.push(DefenseDocuments { + content: get_content(receipt).into(), + content_type: item.shipping_documentation_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(invoice_showing_distinct_transactions) = item.invoice_showing_distinct_transactions + { + defense_documents.push(DefenseDocuments { + content: get_content(invoice_showing_distinct_transactions).into(), + content_type: item.invoice_showing_distinct_transactions_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(customer_communication) = item.customer_communication { + defense_documents.push(DefenseDocuments { + content: get_content(customer_communication).into(), + content_type: item.customer_communication_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(refund_policy) = item.refund_policy { + defense_documents.push(DefenseDocuments { + content: get_content(refund_policy).into(), + content_type: item.refund_policy_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(recurring_transaction_agreement) = item.recurring_transaction_agreement { + defense_documents.push(DefenseDocuments { + content: get_content(recurring_transaction_agreement).into(), + content_type: item.recurring_transaction_agreement_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(uncategorized_file) = item.uncategorized_file { + defense_documents.push(DefenseDocuments { + content: get_content(uncategorized_file).into(), + content_type: item.uncategorized_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(cancellation_policy) = item.cancellation_policy { + defense_documents.push(DefenseDocuments { + content: get_content(cancellation_policy).into(), + content_type: item.cancellation_policy_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(customer_signature) = item.customer_signature { + defense_documents.push(DefenseDocuments { + content: get_content(customer_signature).into(), + content_type: item.customer_signature_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + if let Some(service_documentation) = item.service_documentation { + defense_documents.push(DefenseDocuments { + content: get_content(service_documentation).into(), + content_type: item.service_documentation_file_type, + defense_document_type_code: "DefenseMaterial".into(), + }) + } + + if defense_documents.is_empty() { + None + } else { + Some(defense_documents) + } +} + +fn get_content(item: Vec) -> String { + String::from_utf8_lossy(&item).to_string() +} + +impl ForeignTryFrom<(&Self, AdyenDisputeResponse)> for types::AcceptDisputeRouterData { + type Error = errors::ConnectorError; + + fn foreign_try_from(item: (&Self, AdyenDisputeResponse)) -> Result { + let (data, response) = item; + + if response.success { + Ok(types::AcceptDisputeRouterData { + response: Ok(types::AcceptDisputeResponse { + dispute_status: api_enums::DisputeStatus::DisputeAccepted, + connector_status: None, + }), + ..data.clone() + }) + } else { + Ok(types::AcceptDisputeRouterData { + response: Err(types::ErrorResponse { + code: response + .error_message + .clone() + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .error_message + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.error_message, + status_code: data.connector_http_status_code.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "http code", + }, + )?, + attempt_status: None, + connector_transaction_id: None, + }), + ..data.clone() + }) + } + } +} + +impl ForeignTryFrom<(&Self, AdyenDisputeResponse)> for types::SubmitEvidenceRouterData { + type Error = errors::ConnectorError; + fn foreign_try_from(item: (&Self, AdyenDisputeResponse)) -> Result { + let (data, response) = item; + if response.success { + Ok(types::SubmitEvidenceRouterData { + response: Ok(types::SubmitEvidenceResponse { + dispute_status: api_enums::DisputeStatus::DisputeChallenged, + connector_status: None, + }), + ..data.clone() + }) + } else { + Ok(types::SubmitEvidenceRouterData { + response: Err(types::ErrorResponse { + code: response + .error_message + .clone() + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .error_message + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.error_message, + status_code: data.connector_http_status_code.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "http code", + }, + )?, + attempt_status: None, + connector_transaction_id: None, + }), + ..data.clone() + }) + } + } +} + +impl ForeignTryFrom<(&Self, AdyenDisputeResponse)> for types::DefendDisputeRouterData { + type Error = errors::ConnectorError; + + fn foreign_try_from(item: (&Self, AdyenDisputeResponse)) -> Result { + let (data, response) = item; + + if response.success { + Ok(types::DefendDisputeRouterData { + response: Ok(types::DefendDisputeResponse { + dispute_status: api_enums::DisputeStatus::DisputeChallenged, + connector_status: None, + }), + ..data.clone() + }) + } else { + Ok(types::DefendDisputeRouterData { + response: Err(types::ErrorResponse { + code: response + .error_message + .clone() + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .error_message + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.error_message, + status_code: data.connector_http_status_code.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "http code", + }, + )?, + attempt_status: None, + connector_transaction_id: None, + }), + ..data.clone() + }) + } + } +} diff --git a/crates/router/src/core/disputes/transformers.rs b/crates/router/src/core/disputes/transformers.rs index 7cd878157755..5ddd4cffb1a3 100644 --- a/crates/router/src/core/disputes/transformers.rs +++ b/crates/router/src/core/disputes/transformers.rs @@ -20,34 +20,31 @@ pub async fn get_evidence_request_data( evidence_request: api_models::disputes::SubmitEvidenceRequest, dispute: &diesel_models::dispute::Dispute, ) -> CustomResult { - let (cancellation_policy, cancellation_policy_provider_file_id) = - retrieve_file_and_provider_file_id_from_file_id( - state, - evidence_request.cancellation_policy, - merchant_account, - key_store, - api::FileDataRequired::NotRequired, - ) - .await?; - let (customer_communication, customer_communication_provider_file_id) = - retrieve_file_and_provider_file_id_from_file_id( - state, - evidence_request.customer_communication, - merchant_account, - key_store, - api::FileDataRequired::NotRequired, - ) - .await?; - let (customer_signature, customer_signature_provider_file_id) = - retrieve_file_and_provider_file_id_from_file_id( - state, - evidence_request.customer_signature, - merchant_account, - key_store, - api::FileDataRequired::NotRequired, - ) - .await?; - let (receipt, receipt_provider_file_id) = retrieve_file_and_provider_file_id_from_file_id( + let cancellation_policy_file_info = retrieve_file_and_provider_file_id_from_file_id( + state, + evidence_request.cancellation_policy, + merchant_account, + key_store, + api::FileDataRequired::NotRequired, + ) + .await?; + let customer_communication_file_info = retrieve_file_and_provider_file_id_from_file_id( + state, + evidence_request.customer_communication, + merchant_account, + key_store, + api::FileDataRequired::NotRequired, + ) + .await?; + let customer_sifnature_file_info = retrieve_file_and_provider_file_id_from_file_id( + state, + evidence_request.customer_signature, + merchant_account, + key_store, + api::FileDataRequired::NotRequired, + ) + .await?; + let receipt_file_info = retrieve_file_and_provider_file_id_from_file_id( state, evidence_request.receipt, merchant_account, @@ -55,101 +52,110 @@ pub async fn get_evidence_request_data( api::FileDataRequired::NotRequired, ) .await?; - let (refund_policy, refund_policy_provider_file_id) = - retrieve_file_and_provider_file_id_from_file_id( - state, - evidence_request.refund_policy, - merchant_account, - key_store, - api::FileDataRequired::NotRequired, - ) - .await?; - let (service_documentation, service_documentation_provider_file_id) = - retrieve_file_and_provider_file_id_from_file_id( - state, - evidence_request.service_documentation, - merchant_account, - key_store, - api::FileDataRequired::NotRequired, - ) - .await?; - let (shipping_documentation, shipping_documentation_provider_file_id) = - retrieve_file_and_provider_file_id_from_file_id( - state, - evidence_request.shipping_documentation, - merchant_account, - key_store, - api::FileDataRequired::NotRequired, - ) - .await?; - let ( - invoice_showing_distinct_transactions, - invoice_showing_distinct_transactions_provider_file_id, - ) = retrieve_file_and_provider_file_id_from_file_id( + let refund_policy_file_info = retrieve_file_and_provider_file_id_from_file_id( state, - evidence_request.invoice_showing_distinct_transactions, + evidence_request.refund_policy, merchant_account, key_store, api::FileDataRequired::NotRequired, ) .await?; - let (recurring_transaction_agreement, recurring_transaction_agreement_provider_file_id) = + let service_documentation_file_info = retrieve_file_and_provider_file_id_from_file_id( + state, + evidence_request.service_documentation, + merchant_account, + key_store, + api::FileDataRequired::NotRequired, + ) + .await?; + let shipping_documentation_file_info = retrieve_file_and_provider_file_id_from_file_id( + state, + evidence_request.shipping_documentation, + merchant_account, + key_store, + api::FileDataRequired::NotRequired, + ) + .await?; + let invoice_showing_distinct_transactions_file_info = retrieve_file_and_provider_file_id_from_file_id( state, - evidence_request.recurring_transaction_agreement, + evidence_request.invoice_showing_distinct_transactions, merchant_account, key_store, api::FileDataRequired::NotRequired, ) .await?; - let (uncategorized_file, uncategorized_file_provider_file_id) = + let recurring_transaction_agreement_file_info = retrieve_file_and_provider_file_id_from_file_id( state, - evidence_request.uncategorized_file, + evidence_request.recurring_transaction_agreement, merchant_account, key_store, api::FileDataRequired::NotRequired, ) .await?; + let uncategorized_file_info = retrieve_file_and_provider_file_id_from_file_id( + state, + evidence_request.uncategorized_file, + merchant_account, + key_store, + api::FileDataRequired::NotRequired, + ) + .await?; Ok(SubmitEvidenceRequestData { dispute_id: dispute.dispute_id.clone(), connector_dispute_id: dispute.connector_dispute_id.clone(), access_activity_log: evidence_request.access_activity_log, billing_address: evidence_request.billing_address, - cancellation_policy, - cancellation_policy_provider_file_id, + cancellation_policy: cancellation_policy_file_info.file_data, + cancellation_policy_provider_file_id: cancellation_policy_file_info.provider_file_id, cancellation_policy_disclosure: evidence_request.cancellation_policy_disclosure, cancellation_rebuttal: evidence_request.cancellation_rebuttal, - customer_communication, - customer_communication_provider_file_id, + customer_communication: customer_communication_file_info.file_data, + customer_communication_provider_file_id: customer_communication_file_info.provider_file_id, customer_email_address: evidence_request.customer_email_address, customer_name: evidence_request.customer_name, customer_purchase_ip: evidence_request.customer_purchase_ip, - customer_signature, - customer_signature_provider_file_id, + customer_signature: customer_sifnature_file_info.file_data, + customer_signature_provider_file_id: customer_sifnature_file_info.provider_file_id, product_description: evidence_request.product_description, - receipt, - receipt_provider_file_id, - refund_policy, - refund_policy_provider_file_id, + receipt: receipt_file_info.file_data, + receipt_provider_file_id: receipt_file_info.provider_file_id, + refund_policy: refund_policy_file_info.file_data, + refund_policy_provider_file_id: refund_policy_file_info.provider_file_id, refund_policy_disclosure: evidence_request.refund_policy_disclosure, refund_refusal_explanation: evidence_request.refund_refusal_explanation, service_date: evidence_request.service_date, - service_documentation, - service_documentation_provider_file_id, + service_documentation: service_documentation_file_info.file_data, + service_documentation_provider_file_id: service_documentation_file_info.provider_file_id, shipping_address: evidence_request.shipping_address, shipping_carrier: evidence_request.shipping_carrier, shipping_date: evidence_request.shipping_date, - shipping_documentation, - shipping_documentation_provider_file_id, + shipping_documentation: shipping_documentation_file_info.file_data, + shipping_documentation_provider_file_id: shipping_documentation_file_info.provider_file_id, shipping_tracking_number: evidence_request.shipping_tracking_number, - invoice_showing_distinct_transactions, - invoice_showing_distinct_transactions_provider_file_id, - recurring_transaction_agreement, - recurring_transaction_agreement_provider_file_id, - uncategorized_file, - uncategorized_file_provider_file_id, + invoice_showing_distinct_transactions: invoice_showing_distinct_transactions_file_info + .file_data, + invoice_showing_distinct_transactions_provider_file_id: + invoice_showing_distinct_transactions_file_info.provider_file_id, + recurring_transaction_agreement: recurring_transaction_agreement_file_info.file_data, + recurring_transaction_agreement_provider_file_id: recurring_transaction_agreement_file_info + .provider_file_id, + uncategorized_file: uncategorized_file_info.file_data, + uncategorized_file_provider_file_id: uncategorized_file_info.provider_file_id, uncategorized_text: evidence_request.uncategorized_text, + cancellation_policy_file_type: cancellation_policy_file_info.file_type, + customer_communication_file_type: customer_communication_file_info.file_type, + customer_signature_file_type: customer_sifnature_file_info.file_type, + receipt_file_type: receipt_file_info.file_type, + refund_policy_file_type: refund_policy_file_info.file_type, + service_documentation_file_type: service_documentation_file_info.file_type, + shipping_documentation_file_type: shipping_documentation_file_info.file_type, + invoice_showing_distinct_transactions_file_type: + invoice_showing_distinct_transactions_file_info.file_type, + recurring_transaction_agreement_file_type: recurring_transaction_agreement_file_info + .file_type, + uncategorized_file_type: uncategorized_file_info.file_type, }) } diff --git a/crates/router/src/core/files.rs b/crates/router/src/core/files.rs index 243b93a4483d..5f22416f2be2 100644 --- a/crates/router/src/core/files.rs +++ b/crates/router/src/core/files.rs @@ -110,22 +110,22 @@ pub async fn files_retrieve_core( .await .change_context(errors::ApiErrorResponse::FileNotFound) .attach_printable("Unable to retrieve file_metadata")?; - let (received_data, _provider_file_id) = - helpers::retrieve_file_and_provider_file_id_from_file_id( - &state, - Some(req.file_id), - &merchant_account, - &key_store, - api::FileDataRequired::Required, - ) - .await?; + let file_info = helpers::retrieve_file_and_provider_file_id_from_file_id( + &state, + Some(req.file_id), + &merchant_account, + &key_store, + api::FileDataRequired::Required, + ) + .await?; let content_type = file_metadata_object .file_type .parse::() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to parse file content type")?; Ok(ApplicationResponse::FileData(( - received_data + file_info + .file_data .ok_or(errors::ApiErrorResponse::FileNotAvailable) .attach_printable("File data not found")?, content_type, diff --git a/crates/router/src/core/files/helpers.rs b/crates/router/src/core/files/helpers.rs index 36c71f9a4f75..b479d1315645 100644 --- a/crates/router/src/core/files/helpers.rs +++ b/crates/router/src/core/files/helpers.rs @@ -2,6 +2,7 @@ use actix_multipart::Field; use common_utils::errors::CustomResult; use error_stack::ResultExt; use futures::TryStreamExt; +use hyperswitch_domain_models::router_response_types::disputes::FileInfo; use crate::{ core::{ @@ -175,9 +176,13 @@ pub async fn retrieve_file_and_provider_file_id_from_file_id( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, is_connector_file_data_required: api::FileDataRequired, -) -> CustomResult<(Option>, Option), errors::ApiErrorResponse> { +) -> CustomResult { match file_id { - None => Ok((None, None)), + None => Ok(FileInfo { + file_data: None, + provider_file_id: None, + file_type: None, + }), Some(file_key) => { let file_metadata_object = state .store @@ -194,22 +199,23 @@ pub async fn retrieve_file_and_provider_file_id_from_file_id( .attach_printable("File not available")?, }; match provider { - diesel_models::enums::FileUploadProvider::Router => Ok(( - Some( + diesel_models::enums::FileUploadProvider::Router => Ok(FileInfo { + file_data: Some( state .file_storage_client .retrieve_file(&provider_file_id) .await .change_context(errors::ApiErrorResponse::InternalServerError)?, ), - Some(provider_file_id), - )), + provider_file_id: Some(provider_file_id), + file_type: Some(file_metadata_object.file_type), + }), _ => { let connector_file_data = match is_connector_file_data_required { api::FileDataRequired::Required => Some( retrieve_file_from_connector( state, - file_metadata_object, + file_metadata_object.clone(), merchant_account, key_store, ) @@ -217,7 +223,11 @@ pub async fn retrieve_file_and_provider_file_id_from_file_id( ), api::FileDataRequired::NotRequired => None, }; - Ok((connector_file_data, Some(provider_file_id))) + Ok(FileInfo { + file_data: connector_file_data, + provider_file_id: Some(provider_file_id), + file_type: Some(file_metadata_object.file_type), + }) } } } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 228c7e208cc1..9e724da9feb9 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -623,7 +623,6 @@ impl default_imp_for_accept_dispute!( connector::Adyenplatform, connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bamboraapac, @@ -739,7 +738,6 @@ impl default_imp_for_file_upload!( connector::Adyenplatform, connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bamboraapac, @@ -832,7 +830,6 @@ impl default_imp_for_submit_evidence!( connector::Adyenplatform, connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bamboraapac, @@ -925,7 +922,6 @@ impl default_imp_for_defend_dispute!( connector::Adyenplatform, connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bamboraapac, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 9e5c8a3f0ceb..43eaf4e905a5 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -75,7 +75,8 @@ hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-test.adyen.com/" -adyen.secondary_base_url = "https://pal-test.adyen.com/" +adyen.payout_base_url = "https://pal-test.adyen.com/" +adyen.dispute_base_url = "https://ca-test.adyen.com/" airwallex.base_url = "https://api-demo.airwallex.com/" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api"