From 4905289dd8eb8621ec9371ac6dc539bca00f19e5 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Sun, 3 Dec 2023 23:35:14 +0530 Subject: [PATCH 1/7] update connector documentation --- add_connector.md | 246 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 195 insertions(+), 51 deletions(-) diff --git a/add_connector.md b/add_connector.md index da09ae0024e7..36636705e7af 100644 --- a/add_connector.md +++ b/add_connector.md @@ -9,6 +9,13 @@ 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 Rust installed. We recommend using the nightly version. Install it using `rustup`: + + ```bash + rustup toolchain install nightly + ``` + +Note: This project requires features only available in the nightly version of Rust. In Router, there are Connectors and Payment Methods, examples of both are shown below from which the difference is apparent. @@ -17,7 +24,7 @@ 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. @@ -26,13 +33,21 @@ A connector is an integration to fulfill payments. Related use cases could be an Each Connector (say, a Payment Processor) could support multiple 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. +- **Cards :** Visa, Mastercard, Bancontact , Knet, Mada, Discover, UnionPay +- **Bank Redirects :** Ideal, EPS , Giropay, Sofort, Bancontact, Bizum, Blik, Interac, Onlnie Banking Czec Republic, Onlnie Banking Finland, Onlnie Banking Poland, Onlnie Banking Slovakia, Online Banking UK, Prezelwy24, Trustly, Online Banking Fpx, Online Banking Thailand +- **Bank Transfers :** Multibanco, Sepa, Bacs, Ach, Permata, Bca, Bni, Bri Va, Danamon Va Bank, Pix, Pse +- **Bank Direct Debit :** Sepa direct debit, Ach Debit, Becs Bank Debit, Bacs Bank Debit +- **Wallets :** Apple Pay , Google Pay , Paypal , Ali pay ,Mb pay ,Samsung Pay, Wechat Pay, TouchNGo, Cashapp +- **Card Redirect :** Knet, Benefit, MomoAtm +- **PayLater :** Klarna, Affirm, Afterpay, Paybright, Walley, Alma, Atome +- **Crypto :** Crypto Currency +- **Reward :** Classic +- **Voucher :** Boleto, Efecty, PagoEfectivo, RedCompra, RedPagos, Alfarmart, Indomaret, Oxxo, SevenEleven, Lawson, MiniStop, FamilyMart, Seicomart, PayEasy +- **GiftCard :** Givex, Pay Safe Card +- **Upi :** Upi Collect + +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 +61,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`. @@ -83,16 +97,17 @@ Now let's implement Request type for checkout ```rust #[derive(Debug, Serialize)] -pub struct CheckoutPaymentsRequest { - pub source: Source, +pub struct PaymentsRequest { + pub source: PaymentSource, 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() + pub processing_channel_id: Secret, + #[serde(rename = "3ds")] + pub three_ds: CheckoutThreeDS, + #[serde(flatten)] + pub return_url: ReturnUrl, + pub capture: bool, + pub reference: String, } ``` @@ -105,26 +120,38 @@ Let's define `Source` #[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), } ``` -`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. +`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<'a> From<&types::RouterData<'a>> for CheckoutRequestType +impl TryFrom<&CheckoutRouterData<&T>> for PaymentsRequest +``` +else +```rust +impl TryFrom for PaymentsRequest ``` +where T is genric type which can `types::PaymentsAuthorizeRouterData`, `types::PaymentsCaptureRouterData` etc. + +Note : As amount converion is being handled at one place . amount needs to be consumed from `ConnectorRouterData` + 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,26 +192,40 @@ 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, } } } @@ -195,9 +236,11 @@ Note: `enum::AttemptStatus` is Router status. Router status are given below - **Charged :** The amount has been debited -- **PendingVBV :** Pending but verified by visa +- **Pending :** Pending but verified by visa - **Failure :** The payment Failed -- **Authorizing :** In the process of authorizing. +- **Authorized :** In the process of authorizing. +- **AuthenticationPending :** Customer action is required. +- **Voided :** The payment Cancelled 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 +256,92 @@ 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, +} + +#[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, } -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(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. To implement the redirection flow, assign the redirection link to the `redirection_data`. + And finally the error type implementation ```rust @@ -261,13 +370,16 @@ There are four types of tasks that are done by implementing traits: 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. +- **ConnectorCommon :** contains common description of the connector, like the base endpoint, content-type, error message, id, currency unit. - **Payment :** Trait Relationship, has impl block. - **PaymentAuthorize :** Trait Relationship, has impl block. +- **PaymentCapture :** Trait Relationship, has impl block. +- **PaymentSync :** 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. +- **Refund :** Trait Relationship, has impl block. +- **RefundExecute :** Trait Relationship, has impl block. +- **RefundSync :** Trait Relationship, has impl block. + And the below derive traits @@ -281,6 +393,38 @@ 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, whereas Hyperswitch processes amounts in the minor unit. 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. + ### **Test the connector** Try running the tests in `crates/router/tests/connectors/{{connector-name}}.rs`. From a81327c32258dc3607200141668b36d0926c0e44 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:38:44 +0530 Subject: [PATCH 2/7] update add_connector file Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> --- add_connector.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/add_connector.md b/add_connector.md index 36636705e7af..a717071cc0cb 100644 --- a/add_connector.md +++ b/add_connector.md @@ -230,10 +230,11 @@ impl ForeignFrom<(CheckoutPaymentStatus, Option)> for enums::AttemptSt } } ``` +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. -Router status are given below +Some of the router status are given below - **Charged :** The amount has been debited - **Pending :** Pending but verified by visa From 11202b3345d1ff79c2d57d3fb41b195f9732ee2b Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:42:14 +0530 Subject: [PATCH 3/7] fix spell check Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> --- add_connector.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/add_connector.md b/add_connector.md index a717071cc0cb..b37ccff6ad81 100644 --- a/add_connector.md +++ b/add_connector.md @@ -148,9 +148,9 @@ else impl TryFrom for PaymentsRequest ``` -where T is genric type which can `types::PaymentsAuthorizeRouterData`, `types::PaymentsCaptureRouterData` etc. +where T is generic type which can `types::PaymentsAuthorizeRouterData`, `types::PaymentsCaptureRouterData` etc. -Note : As amount converion is being handled at one place . amount needs to be consumed from `ConnectorRouterData` +Note : As amount conversion is being handled at one place . amount needs to be consumed from `ConnectorRouterData` 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. From e272233255e9df547277519b91ff7cfa455e0d2a Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:44:28 +0530 Subject: [PATCH 4/7] resolve comments --- add_connector.md | 81 +++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/add_connector.md b/add_connector.md index b37ccff6ad81..68ea464223e1 100644 --- a/add_connector.md +++ b/add_connector.md @@ -9,13 +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 Rust installed. We recommend using the nightly version. Install it using `rustup`: +- 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 ``` -Note: This project requires features only available in the nightly version of Rust. In Router, there are Connectors and Payment Methods, examples of both are shown below from which the difference is apparent. @@ -34,7 +35,7 @@ A connector is an integration to fulfill payments. Related use cases could be an Each Connector (say, a Payment Processor) could support multiple payment methods - **Cards :** Visa, Mastercard, Bancontact , Knet, Mada, Discover, UnionPay -- **Bank Redirects :** Ideal, EPS , Giropay, Sofort, Bancontact, Bizum, Blik, Interac, Onlnie Banking Czec Republic, Onlnie Banking Finland, Onlnie Banking Poland, Onlnie Banking Slovakia, Online Banking UK, Prezelwy24, Trustly, Online Banking Fpx, Online Banking Thailand +**Bank Redirects :** Ideal, EPS , Giropay, Sofort, Bancontact, Bizum, Blik, Interac, Online Banking Czech Republic, Online Banking Finland, Online Banking Poland, Online Banking Slovakia, Online Banking UK, Prezelwy24, Trustly, Online Banking Fpx, Online Banking Thailand - **Bank Transfers :** Multibanco, Sepa, Bacs, Ach, Permata, Bca, Bni, Bri Va, Danamon Va Bank, Pix, Pse - **Bank Direct Debit :** Sepa direct debit, Ach Debit, Becs Bank Debit, Bacs Bank Debit - **Wallets :** Apple Pay , Google Pay , Paypal , Ali pay ,Mb pay ,Samsung Pay, Wechat Pay, TouchNGo, Cashapp @@ -96,6 +97,24 @@ 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 CardSource { + #[serde(rename = "type")] + 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 PaymentSource { + Card(CardSource), + Wallets(WalletSource), + ApplePayPredecrypt(Box), +} + #[derive(Debug, Serialize)] pub struct PaymentsRequest { pub source: PaymentSource, @@ -116,26 +135,6 @@ Since Router is connector agnostic, only minimal data is sent to connector and o 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: CheckoutSourceTypes, - pub number: cards::CardNumber, - pub expiry_month: Secret, - pub expiry_year: Secret, - pub cvv: Secret, -} - -#[derive(Debug, Serialize)] -#[serde(untagged)] -pub enum PaymentSource { - Card(CardSource), - Wallets(WalletSource), - ApplePayPredecrypt(Box), -} -``` - `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: @@ -148,9 +147,7 @@ else impl TryFrom for PaymentsRequest ``` -where T is generic type which can `types::PaymentsAuthorizeRouterData`, `types::PaymentsCaptureRouterData` etc. - -Note : As amount conversion is being handled at one place . amount needs to be consumed from `ConnectorRouterData` +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. @@ -232,16 +229,16 @@ impl ForeignFrom<(CheckoutPaymentStatus, Option)> for enums::AttemptSt ``` 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. -Some of the router status are given below +Some of the attempt status are given below -- **Charged :** The amount has been debited -- **Pending :** Pending but verified by visa -- **Failure :** The payment Failed -- **Authorized :** 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 Cancelled +- **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. @@ -372,14 +369,14 @@ 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, currency unit. -- **Payment :** Trait Relationship, has impl block. -- **PaymentAuthorize :** Trait Relationship, has impl block. -- **PaymentCapture :** Trait Relationship, has impl block. -- **PaymentSync :** 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 impl block. -- **RefundExecute :** Trait Relationship, has impl block. -- **RefundSync :** Trait Relationship, has impl block. +- **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 @@ -395,7 +392,7 @@ Refer to other connector code for trait implementations. Mostly the rust compile 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, whereas Hyperswitch processes amounts in the minor unit. If a connector accepts amounts in the base unit, conversion is required, as illustrated. +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 @@ -424,7 +421,7 @@ impl } ``` -`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. +**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. ### **Test the connector** From 56123c35bc9c22525acc825115b54a95da444bbd Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:36:40 +0530 Subject: [PATCH 5/7] resolve comments --- add_connector.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/add_connector.md b/add_connector.md index 68ea464223e1..b6c8b2f71687 100644 --- a/add_connector.md +++ b/add_connector.md @@ -133,7 +133,7 @@ pub struct PaymentsRequest { 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` +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. From 925d37f73e200d116656211bae545c8725060a2d Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:03:12 +0530 Subject: [PATCH 6/7] update add_connector.md --- add_connector.md | 367 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 333 insertions(+), 34 deletions(-) diff --git a/add_connector.md b/add_connector.md index b6c8b2f71687..e131732a4111 100644 --- a/add_connector.md +++ b/add_connector.md @@ -13,9 +13,9 @@ This is a guide to contributing new connector to Router. This guide includes ins Install it using `rustup`: - ```bash + ```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. @@ -32,20 +32,7 @@ A connector is an integration to fulfill payments. Related use cases could be an ### What is a Payment Method ? -Each Connector (say, a Payment Processor) could support multiple payment methods - -- **Cards :** Visa, Mastercard, Bancontact , Knet, Mada, Discover, UnionPay -**Bank Redirects :** Ideal, EPS , Giropay, Sofort, Bancontact, Bizum, Blik, Interac, Online Banking Czech Republic, Online Banking Finland, Online Banking Poland, Online Banking Slovakia, Online Banking UK, Prezelwy24, Trustly, Online Banking Fpx, Online Banking Thailand -- **Bank Transfers :** Multibanco, Sepa, Bacs, Ach, Permata, Bca, Bni, Bri Va, Danamon Va Bank, Pix, Pse -- **Bank Direct Debit :** Sepa direct debit, Ach Debit, Becs Bank Debit, Bacs Bank Debit -- **Wallets :** Apple Pay , Google Pay , Paypal , Ali pay ,Mb pay ,Samsung Pay, Wechat Pay, TouchNGo, Cashapp -- **Card Redirect :** Knet, Benefit, MomoAtm -- **PayLater :** Klarna, Affirm, Afterpay, Paybright, Walley, Alma, Atome -- **Crypto :** Crypto Currency -- **Reward :** Classic -- **Voucher :** Boleto, Efecty, PagoEfectivo, RedCompra, RedPagos, Alfarmart, Indomaret, Oxxo, SevenEleven, Lawson, MiniStop, FamilyMart, Seicomart, PayEasy -- **GiftCard :** Givex, Pay Safe Card -- **Upi :** Upi Collect +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. 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. @@ -338,7 +325,34 @@ impl TryFrom> } ``` -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. To implement the redirection flow, assign the redirection link to the `redirection_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 @@ -358,25 +372,249 @@ 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, currency unit. -- **ConnectorIntegration :** For every api endpoint contains the url, using request transform and response transform and headers. -- **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. +**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 @@ -423,9 +661,70 @@ impl **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** From 87ac9ee12d20be89240d0aa4b4508980a933596b Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:39:39 +0530 Subject: [PATCH 7/7] resolve comments --- add_connector.md | 321 +++++++++++++++++++++++------------------------ 1 file changed, 160 insertions(+), 161 deletions(-) diff --git a/add_connector.md b/add_connector.md index e131732a4111..7fc3dcb27d14 100644 --- a/add_connector.md +++ b/add_connector.md @@ -28,7 +28,7 @@ A connector is an integration to fulfill payments. Related use cases could be an - 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 ? @@ -340,7 +340,7 @@ Some recommended fields that needs to be set on connector request and response 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` . +- **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()), @@ -349,8 +349,8 @@ Some recommended fields that needs to be set on connector request and response ```rust let redirection_data = item.response.links.redirect.map(|href| { - services::RedirectForm::from((href.redirection_url, services::Method::Get)) - }); + services::RedirectForm::from((href.redirection_url, services::Method::Get)) + }); ``` @@ -382,203 +382,203 @@ 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" - } + "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 - } + 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" - } + "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(), - )]) - } + &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() - } + 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, - }) - } + &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 ): +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")) - } + &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) - } + &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. +- `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)) - } + &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(), - )) - } + &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) - } + &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) - } + &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. @@ -587,18 +587,18 @@ Within the `ConnectorIntegration` trait, you'll find the following methods imple 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) - } + &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) + } } ``` @@ -625,7 +625,6 @@ 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.