mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
docs(connector): update connector integration documentation (#3041)
Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
This commit is contained in:
602
add_connector.md
602
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 <connector-name> <connector-base-url>
|
||||
sh scripts/add_connector.sh <connector-name> <connector-base-url>
|
||||
```
|
||||
|
||||
For this tutorial `<connector-name>` would be `checkout`.
|
||||
@ -83,48 +85,57 @@ 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>,
|
||||
pub struct CardSource {
|
||||
#[serde(rename = "type")]
|
||||
pub source_type: CheckoutSourceTypes,
|
||||
pub number: cards::CardNumber,
|
||||
pub expiry_month: Secret<String>,
|
||||
pub expiry_year: Secret<String>,
|
||||
pub cvv: Secret<String>,
|
||||
}
|
||||
|
||||
fn generate_processing_channel_id() -> Cow<'static, str> {
|
||||
"pc_e4mrdrifohhutfurvuawughfwu".into()
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PaymentSource {
|
||||
Card(CardSource),
|
||||
Wallets(WalletSource),
|
||||
ApplePayPredecrypt(Box<ApplePayPredecrypt>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PaymentsRequest {
|
||||
pub source: PaymentSource,
|
||||
pub amount: i64,
|
||||
pub currency: String,
|
||||
pub processing_channel_id: Secret<String>,
|
||||
#[serde(rename = "3ds")]
|
||||
pub three_ds: CheckoutThreeDS,
|
||||
#[serde(flatten)]
|
||||
pub return_url: ReturnUrl,
|
||||
pub capture: bool,
|
||||
pub reference: String,
|
||||
}
|
||||
```
|
||||
|
||||
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<RouterData>` needs to be implemented.
|
||||
|
||||
For request types that involve an amount, the implementation of `TryFrom<&ConnectorRouterData<&T>>` is required:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CardSource {
|
||||
#[serde(rename = "type")]
|
||||
pub source_type: Option<String>,
|
||||
pub number: Option<String>,
|
||||
pub expiry_month: Option<String>,
|
||||
pub expiry_year: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Source {
|
||||
Card(CardSource),
|
||||
// TODO: Add other sources here.
|
||||
}
|
||||
impl TryFrom<&CheckoutRouterData<&T>> for PaymentsRequest
|
||||
```
|
||||
|
||||
`Source` is an enum type. Request types will need to derive `Serialize` and response types will need to derive `Deserialize`. For request types `From<RouterData>` needs to be implemented.
|
||||
|
||||
else
|
||||
```rust
|
||||
impl<'a> From<&types::RouterData<'a>> for CheckoutRequestType
|
||||
impl TryFrom<T> 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<CheckoutPaymentStatus> 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<Balances>)> for enums::AttemptStatus {
|
||||
fn foreign_from(item: (CheckoutPaymentStatus, Option<Balances>)) -> 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<ConnectorPaymentStatus> 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<i32>,
|
||||
action_id: Option<String>,
|
||||
status: CheckoutPaymentStatus,
|
||||
#[serde(rename = "_links")]
|
||||
links: Links,
|
||||
balances: Option<Balances>,
|
||||
reference: Option<String>,
|
||||
response_code: Option<String>,
|
||||
response_summary: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> From<types::ResponseRouterData<'a, CheckoutPaymentsResponse>> 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<bool>,
|
||||
pub reference: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PaymentsResponseEnum {
|
||||
ActionResponse(Vec<ActionResponse>),
|
||||
PaymentResponse(Box<PaymentsResponse>),
|
||||
}
|
||||
|
||||
impl TryFrom<types::PaymentsResponseRouterData<PaymentsResponse>>
|
||||
for types::PaymentsAuthorizeRouterData
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::PaymentsResponseRouterData<PaymentsResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<Vec<(String, request::Maskable<String>)>, 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<types::ErrorResponse, errors::ConnectorError> {
|
||||
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<String, errors::ConnectorError> {
|
||||
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<Vec<(String, request::Maskable<String>)>, 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<Option<types::RequestBody>, 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::<checkout::PaymentsRequest>::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<Option<services::Request>, 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<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
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<types::ErrorResponse, errors::ConnectorError> {
|
||||
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<Flow, Request, Response>,
|
||||
{
|
||||
fn build_headers(
|
||||
&self,
|
||||
req: &types::RouterData<Flow, Request, Response>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<T>
|
||||
TryFrom<(
|
||||
&types::api::CurrencyUnit,
|
||||
types::storage::enums::Currency,
|
||||
i64,
|
||||
T,
|
||||
)> for PaypalRouterData<T>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
(currency_unit, currency, amount, item): (
|
||||
&types::api::CurrencyUnit,
|
||||
types::storage::enums::Currency,
|
||||
i64,
|
||||
T,
|
||||
),
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<types::PaymentsAuthorizeData>,
|
||||
payment_info: Option<PaymentInfo>,
|
||||
) -> Result<types::PaymentsAuthorizeRouterData, Report<ConnectorError>> {
|
||||
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**
|
||||
|
||||
Reference in New Issue
Block a user