diff --git a/add_connector.md b/add_connector.md index da09ae0024e7..7fc3dcb27d14 100644 --- a/add_connector.md +++ b/add_connector.md @@ -9,6 +9,14 @@ This is a guide to contributing new connector to Router. This guide includes ins - Understanding of the Connector APIs which you wish to integrate with Router - Setup of Router repository and running it on local - Access to API credentials for testing the Connector API (you can quickly sign up for sandbox/uat credentials by visiting the website of the connector you wish to integrate) +- Ensure that you have the nightly toolchain installed because the connector template script includes code formatting. + + Install it using `rustup`: + + ```bash + rustup toolchain install nightly + ``` + In Router, there are Connectors and Payment Methods, examples of both are shown below from which the difference is apparent. @@ -17,22 +25,17 @@ In Router, there are Connectors and Payment Methods, examples of both are shown A connector is an integration to fulfill payments. Related use cases could be any of the below - Payment processor (Stripe, Adyen, ChasePaymentTech etc.,) -- Fraud and Risk management platform (like Ravelin, Riskified etc.,) +- Fraud and Risk management platform (like Signifyd, Riskified etc.,) - Payment network (Visa, Master) - Payment authentication services (Cardinal etc.,) - Router supports "Payment Processors" right now. Support will be extended to the other categories in the near future. +Currently, the router is compatible with 'Payment Processors' and 'Fraud and Risk Management' platforms. Support for additional categories will be expanded in the near future. ### What is a Payment Method ? -Each Connector (say, a Payment Processor) could support multiple payment methods +Every Payment Processor has the capability to accommodate various payment methods. Refer to the [Hyperswitch Payment matrix](https://hyperswitch.io/pm-list) to discover the supported processors and payment methods. -- **Cards :** Bancontact , Knet, Mada -- **Bank Transfers :** EPS , giropay, sofort -- **Bank Direct Debit :** Sepa direct debit -- **Wallets :** Apple Pay , Google Pay , Paypal - -Cards and Bank Transfer payment methods are already included in Router. Hence, adding a new connector which offers payment_methods available in Router is easy and requires almost no breaking changes. -Adding a new payment method (say Wallets or Bank Direct Debit) might require some changes in core business logic of Router, which we are actively working upon. +The above mentioned payment methods are already included in Router. Hence, adding a new connector which offers payment_methods available in Router is easy and requires almost no breaking changes. +Adding a new payment method might require some changes in core business logic of Router, which we are actively working upon. ## How to Integrate a Connector @@ -46,8 +49,7 @@ Below is a step-by-step tutorial for integrating a new connector. ### **Generate the template** ```bash -cd scripts -bash add_connector.sh +sh scripts/add_connector.sh ``` For this tutorial `` would be `checkout`. @@ -81,50 +83,59 @@ For example, in case of checkout, the [request](https://api-reference.checkout.c Now let's implement Request type for checkout -```rust -#[derive(Debug, Serialize)] -pub struct CheckoutPaymentsRequest { - pub source: Source, - pub amount: i64, - pub currency: String, - #[serde(default = "generate_processing_channel_id")] - pub processing_channel_id: Cow<'static, str>, -} - -fn generate_processing_channel_id() -> Cow<'static, str> { - "pc_e4mrdrifohhutfurvuawughfwu".into() -} -``` - -Since Router is connector agnostic, only minimal data is sent to connector and optional fields may be ignored. - -Here processing_channel_id, is specific to checkout and implementations of such functions should be inside the checkout directory. -Let's define `Source` - ```rust #[derive(Debug, Serialize)] pub struct CardSource { #[serde(rename = "type")] - pub source_type: Option, - pub number: Option, - pub expiry_month: Option, - pub expiry_year: Option, + pub source_type: CheckoutSourceTypes, + pub number: cards::CardNumber, + pub expiry_month: Secret, + pub expiry_year: Secret, + pub cvv: Secret, } #[derive(Debug, Serialize)] #[serde(untagged)] -pub enum Source { +pub enum PaymentSource { Card(CardSource), - // TODO: Add other sources here. + Wallets(WalletSource), + ApplePayPredecrypt(Box), +} + +#[derive(Debug, Serialize)] +pub struct PaymentsRequest { + pub source: PaymentSource, + pub amount: i64, + pub currency: String, + pub processing_channel_id: Secret, + #[serde(rename = "3ds")] + pub three_ds: CheckoutThreeDS, + #[serde(flatten)] + pub return_url: ReturnUrl, + pub capture: bool, + pub reference: String, } ``` -`Source` is an enum type. Request types will need to derive `Serialize` and response types will need to derive `Deserialize`. For request types `From` needs to be implemented. +Since Router is connector agnostic, only minimal data is sent to connector and optional fields may be ignored. + +Here processing_channel_id, is specific to checkout and implementations of such functions should be inside the checkout directory. +Let's define `PaymentSource` + +`PaymentSource` is an enum type. Request types will need to derive `Serialize` and response types will need to derive `Deserialize`. For request types `From` needs to be implemented. +For request types that involve an amount, the implementation of `TryFrom<&ConnectorRouterData<&T>>` is required: + +```rust +impl TryFrom<&CheckoutRouterData<&T>> for PaymentsRequest +``` +else ```rust -impl<'a> From<&types::RouterData<'a>> for CheckoutRequestType +impl TryFrom for PaymentsRequest ``` +where `T` is a generic type which can be `types::PaymentsAuthorizeRouterData`, `types::PaymentsCaptureRouterData`, etc. + In this impl block we build the request type from RouterData which will almost always contain all the required information you need for payment processing. `RouterData` contains all the information required for processing the payment. @@ -165,39 +176,56 @@ While implementing the Response Type, the important Enum to be defined for every It stores the different status types that the connector can give in its response that is listed in its API spec. Below is the definition for checkout ```rust -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Default, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] pub enum CheckoutPaymentStatus { Authorized, + #[default] Pending, #[serde(rename = "Card Verified")] CardVerified, Declined, + Captured, } ``` The important part is mapping it to the Router status codes. ```rust -impl From for enums::AttemptStatus { - fn from(item: CheckoutPaymentStatus) -> Self { - match item { - CheckoutPaymentStatus::Authorized => enums::AttemptStatus::Charged, - CheckoutPaymentStatus::Declined => enums::AttemptStatus::Failure, - CheckoutPaymentStatus::Pending => enums::AttemptStatus::Authorizing, - CheckoutPaymentStatus::CardVerified => enums::AttemptStatus::Pending, +impl ForeignFrom<(CheckoutPaymentStatus, Option)> for enums::AttemptStatus { + fn foreign_from(item: (CheckoutPaymentStatus, Option)) -> Self { + let (status, balances) = item; + + match status { + CheckoutPaymentStatus::Authorized => { + if let Some(Balances { + available_to_capture: 0, + }) = balances + { + Self::Charged + } else { + Self::Authorized + } + } + CheckoutPaymentStatus::Captured => Self::Charged, + CheckoutPaymentStatus::Declined => Self::Failure, + CheckoutPaymentStatus::Pending => Self::AuthenticationPending, + CheckoutPaymentStatus::CardVerified => Self::Pending, } } } ``` +If you're converting ConnectorPaymentStatus to AttemptStatus without any additional conditions, you can employ the `impl From for enums::AttemptStatus`. -Note: `enum::AttemptStatus` is Router status. +Note: A payment intent can have multiple payment attempts. `enums::AttemptStatus` represents the status of a payment attempt. -Router status are given below +Some of the attempt status are given below -- **Charged :** The amount has been debited -- **PendingVBV :** Pending but verified by visa -- **Failure :** The payment Failed -- **Authorizing :** In the process of authorizing. +- **Charged :** The payment attempt has succeeded. +- **Pending :** Payment is in processing state. +- **Failure :** The payment attempt has failed. +- **Authorized :** Payment is authorized. Authorized payment can be voided, captured and partial captured. +- **AuthenticationPending :** Customer action is required. +- **Voided :** The payment was voided and never captured; the funds were returned to the customer. It is highly recommended that the default status is Pending. Only explicit failure and explicit success from the connector shall be marked as success or failure respectively. @@ -213,26 +241,119 @@ impl Default for CheckoutPaymentStatus { Below is rest of the response type implementation for checkout ```rust - -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct CheckoutPaymentsResponse { +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct PaymentsResponse { id: String, - amount: i64, + amount: Option, + action_id: Option, status: CheckoutPaymentStatus, + #[serde(rename = "_links")] + links: Links, + balances: Option, + reference: Option, + response_code: Option, + response_summary: Option, } -impl<'a> From> for types::RouterData<'a> { - fn from(item: types::ResponseRouterData<'a, CheckoutPaymentsResponse>) -> Self { - types::RouterData { - connector_transaction_id: Some(item.response.id), - amount_received: Some(item.response.amount), - status: enums::Status::from(item.response.status), +#[derive(Deserialize, Debug)] +pub struct ActionResponse { + #[serde(rename = "id")] + pub action_id: String, + pub amount: i64, + #[serde(rename = "type")] + pub action_type: ActionType, + pub approved: Option, + pub reference: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum PaymentsResponseEnum { + ActionResponse(Vec), + PaymentResponse(Box), +} + +impl TryFrom> + for types::PaymentsAuthorizeRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsResponseRouterData, + ) -> Result { + let redirection_data = item.response.links.redirect.map(|href| { + services::RedirectForm::from((href.redirection_url, services::Method::Get)) + }); + let status = enums::AttemptStatus::foreign_from(( + item.response.status, + item.data.request.capture_method, + )); + let error_response = if status == enums::AttemptStatus::Failure { + Some(types::ErrorResponse { + status_code: item.http_code, + code: item + .response + .response_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: item + .response + .response_summary + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.response_summary, + attempt_status: None, + connector_transaction_id: None, + }) + } else { + None + }; + let payments_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), + redirection_data, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + item.response.reference.unwrap_or(item.response.id), + ), + }; + Ok(Self { + status, + response: error_response.map_or_else(|| Ok(payments_response_data), Err), ..item.data - } + }) } } ``` +Using an enum for a response struct in Rust is not recommended due to potential deserialization issues where the deserializer attempts to deserialize into all the enum variants. A preferable alternative is to employ a separate enum for the possible response variants and include it as a field within the response struct. + +Some recommended fields that needs to be set on connector request and response + +- **connector_request_reference_id :** Most of the connectors anticipate merchants to include their own reference ID in payment requests. For instance, the merchant's reference ID in the checkout `PaymentRequest` is specified as `reference`. + +```rust + reference: item.router_data.connector_request_reference_id.clone(), +``` +- **connector_response_reference_id :** Merchants might face ambiguity when deciding which ID to use in the connector dashboard for payment identification. It is essential to populate the connector_response_reference_id with the appropriate reference ID, allowing merchants to recognize the transaction. This field can be linked to either `merchant_reference` or `connector_transaction_id`, depending on the field that the connector dashboard search functionality supports. + +```rust + connector_response_reference_id: item.response.reference.or(Some(item.response.id)) +``` + +- **resource_id :** The connector assigns an identifier to a payment attempt, referred to as `connector_transaction_id`. This identifier is represented as an enum variant for the `resource_id`. If the connector does not provide a `connector_transaction_id`, the resource_id is set to `NoResponseId`. + +```rust + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), +``` +- **redirection_data :** For the implementation of a redirection flow (3D Secure, bank redirects, etc.), assign the redirection link to the `redirection_data`. + +```rust + let redirection_data = item.response.links.redirect.map(|href| { + services::RedirectForm::from((href.redirection_url, services::Method::Get)) + }); +``` + + And finally the error type implementation ```rust @@ -251,23 +372,250 @@ Similarly for every API endpoint you can implement request and response types. The `mod.rs` file contains the trait implementations where we use the types in transformers. -There are four types of tasks that are done by implementing traits: - -- **Payment :** For making/initiating payments -- **PaymentSync :** For checking status of the payment -- **Refund :** For initiating refund -- **RefundSync :** For checking status of the Refund. - We create a struct with the connector name and have trait implementations for it. The following trait implementations are mandatory -- **ConnectorCommon :** contains common description of the connector, like the base endpoint, content-type, error message, id. -- **Payment :** Trait Relationship, has impl block. -- **PaymentAuthorize :** Trait Relationship, has impl block. -- **ConnectorIntegration :** For every api endpoint contains the url, using request transform and response transform and headers. -- **Refund :** Trait Relationship, has empty body. -- **RefundExecute :** Trait Relationship, has empty body. -- **RefundSync :** Trait Relationship, has empty body. +**ConnectorCommon :** contains common description of the connector, like the base endpoint, content-type, error response handling, id, currency unit. + +Within the `ConnectorCommon` trait, you'll find the following methods : + + - `id` method corresponds directly to the connector name. + ```rust + fn id(&self) -> &'static str { + "checkout" + } + ``` + - `get_currency_unit` method anticipates you to [specify the accepted currency unit](#set-the-currency-unit) for the connector. + ```rust + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + ``` + - `common_get_content_type` method requires you to provide the accepted content type for the connector API. + ```rust + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + ``` + - `get_auth_header` method accepts common HTTP Authorization headers that are accepted in all `ConnectorIntegration` flows. + ```rust + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth: checkout::CheckoutAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", auth.api_secret.peek()).into_masked(), + )]) + } + ``` + + - `base_url` method is for fetching the base URL of connector's API. Base url needs to be consumed from configs. + ```rust + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.checkout.base_url.as_ref() + } + ``` + - `build_error_response` method is common error response handling for a connector if it is same in all cases + + ```rust + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: checkout::ErrorResponse = if res.response.is_empty() { + let (error_codes, error_type) = if res.status_code == 401 { + ( + Some(vec!["Invalid api key".to_string()]), + Some("invalid_api_key".to_string()), + ) + } else { + (None, None) + }; + checkout::ErrorResponse { + request_id: None, + error_codes, + error_type, + } + } else { + res.response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)? + }; + + router_env::logger::info!(error_response=?response); + let errors_list = response.error_codes.clone().unwrap_or_default(); + let option_error_code_message = conn_utils::get_error_code_error_message_based_on_priority( + self.clone(), + errors_list + .into_iter() + .map(|errors| errors.into()) + .collect(), + ); + Ok(types::ErrorResponse { + status_code: res.status_code, + code: option_error_code_message + .clone() + .map(|error_code_message| error_code_message.error_code) + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: option_error_code_message + .map(|error_code_message| error_code_message.error_message) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: response + .error_codes + .map(|errors| errors.join(" & ")) + .or(response.error_type), + attempt_status: None, + connector_transaction_id: None, + }) + } + ``` + +**ConnectorIntegration :** For every api endpoint contains the url, using request transform and response transform and headers. +Within the `ConnectorIntegration` trait, you'll find the following methods implemented(below mentioned is example for authorized flow): + +- `get_url` method defines endpoint for authorize flow, base url is consumed from `ConnectorCommon` trait. + +```rust + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "payments")) + } +``` +- `get_headers` method accepts HTTP headers that are accepted for authorize flow. In this context, it is utilized from the `ConnectorCommonExt` trait, as the connector adheres to common headers across various flows. + +```rust + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } +``` + +- `get_request_body` method calls transformers where hyperswitch payment request data is transformed into connector payment request. For constructing the request body have a function `log_and_get_request_body` that allows generic argument which is the struct that is passed as the body for connector integration, and a function that can be use to encode it into String. We log the request in this function, as the struct will be intact and the masked values will be masked. + +```rust + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = checkout::CheckoutRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = checkout::PaymentsRequest::try_from(&connector_router_data)?; + let checkout_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(checkout_req)) + } +``` + +- `build_request` method assembles the API request by providing the method, URL, headers, and request body as parameters. +```rust + fn build_request( + &self, + req: &types::RouterData< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + 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, + )?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } +``` +- `handle_response` method calls transformers where connector response data is transformed into hyperswitch response. +```rust + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: types::Response, + ) -> CustomResult { + let response: checkout::PaymentsResponse = res + .response + .parse_struct("PaymentIntentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } +``` +- `get_error_response` method to manage error responses. As the handling of checkout errors remains consistent across various flows, we've incorporated it from the `build_error_response` method within the `ConnectorCommon` trait. +```rust + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +``` +**ConnectorCommonExt :** An enhanced trait for `ConnectorCommon` that enables functions with a generic type. This trait includes the `build_headers` method, responsible for constructing both the common headers and the Authorization headers (retrieved from the `get_auth_header` method), returning them as a vector. + +```rust + where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let 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) + } +} +``` + +**Payment :** This trait includes several other traits and is meant to represent the functionality related to payments. + +**PaymentAuthorize :** This trait extends the `api::ConnectorIntegration `trait with specific types related to payment authorization. + +**PaymentCapture :** This trait extends the `api::ConnectorIntegration `trait with specific types related to manual payment capture. + +**PaymentSync :** This trait extends the `api::ConnectorIntegration `trait with specific types related to payment retrieve. + +**Refund :** This trait includes several other traits and is meant to represent the functionality related to Refunds. + +**RefundExecute :** This trait extends the `api::ConnectorIntegration `trait with specific types related to refunds create. + +**RefundSync :** This trait extends the `api::ConnectorIntegration `trait with specific types related to refunds retrieve. + And the below derive traits @@ -277,13 +625,105 @@ And the below derive traits There is a trait bound to implement refunds, if you don't want to implement refunds you can mark them as `todo!()` but code panics when you initiate refunds then. -Don’t forget to add logs lines in appropriate places. Refer to other connector code for trait implementations. Mostly the rust compiler will guide you to do it easily. Feel free to connect with us in case of any queries and if you want to confirm the status mapping. +### **Set the currency Unit** +The `get_currency_unit` function, part of the ConnectorCommon trait, enables connectors to specify their accepted currency unit as either `Base` or `Minor`. For instance, Paypal designates its currency in the base unit (for example, USD), whereas Hyperswitch processes amounts in the minor unit (for example, cents). If a connector accepts amounts in the base unit, conversion is required, as illustrated. + +``` rust +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for PaypalRouterData +{ + type Error = error_stack::Report; + fn try_from( + (currency_unit, currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data: item, + }) + } +} +``` + +**Note:** Since the amount is being converted in the aforementioned `try_from`, it is necessary to retrieve amounts from `ConnectorRouterData` in all other `try_from` instances. + +### **Connector utility functions** + +In the `connector/utils.rs` file, you'll discover utility functions that aid in constructing connector requests and responses. We highly recommend using these helper functions for retrieving payment request fields, such as `get_billing_country`, `get_browser_info`, and `get_expiry_date_as_yyyymm`, as well as for validations, including `is_three_ds`, `is_auto_capture`, and more. + +```rust + let json_wallet_data: CheckoutGooglePayData =wallet_data.get_wallet_token_as_json()?; +``` + ### **Test the connector** -Try running the tests in `crates/router/tests/connectors/{{connector-name}}.rs`. +The template code script generates a test file for the connector, containing 20 sanity tests. We anticipate that you will implement these tests when adding a new connector. + +```rust +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} +``` + +Utility functions for tests are also available at `tests/connector/utils`. These functions enable you to write tests with ease. + +```rust + /// For initiating payments when `CaptureMethod` is set to `Manual` + /// This doesn't complete the transaction, `PaymentsCapture` needs to be done manually + async fn authorize_payment( + &self, + payment_data: Option, + payment_info: Option, + ) -> Result> { + let integration = self.get_data().connector.get_connector_integration(); + let mut request = self.generate_data( + types::PaymentsAuthorizeData { + confirm: true, + capture_method: Some(diesel_models::enums::CaptureMethod::Manual), + ..(payment_data.unwrap_or(PaymentAuthorizeType::default().0)) + }, + payment_info, + ); + let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( + Settings::new().unwrap(), + StorageImpl::PostgresqlTest, + tx, + Box::new(services::MockApiClient), + ) + .await; + integration.execute_pretasks(&mut request, &state).await?; + Box::pin(call_connector(request, integration)).await + } +``` + +Prior to executing tests in the shell, ensure that the API keys are configured in `crates/router/tests/connectors/sample_auth.toml` and set the environment variable `CONNECTOR_AUTH_FILE_PATH` using the export command. Avoid pushing code with exposed API keys. + +```rust + export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml" + cargo test --package router --test connectors -- checkout --test-threads=1 +``` All tests should pass and add appropriate tests for connector specific payment flows. ### **Build payment request and response from json schema**