mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
feat(connector): implement authorize and capture flows for Fiserv (#266)
This commit is contained in:
@ -43,7 +43,7 @@ locker_decryption_key2 = ""
|
|||||||
|
|
||||||
[connectors.supported]
|
[connectors.supported]
|
||||||
wallets = ["klarna","braintree","applepay"]
|
wallets = ["klarna","braintree","applepay"]
|
||||||
cards = ["stripe","adyen","authorizedotnet","checkout","braintree","aci","shift4","cybersource", "worldpay", "globalpay"]
|
cards = ["stripe","adyen","authorizedotnet","checkout","braintree","aci","shift4","cybersource", "worldpay", "globalpay", "fiserv"]
|
||||||
|
|
||||||
[refund]
|
[refund]
|
||||||
max_attempts = 10
|
max_attempts = 10
|
||||||
@ -82,6 +82,9 @@ base_url = "https://apitest.cybersource.com/"
|
|||||||
[connectors.shift4]
|
[connectors.shift4]
|
||||||
base_url = "https://api.shift4.com/"
|
base_url = "https://api.shift4.com/"
|
||||||
|
|
||||||
|
[connectors.fiserv]
|
||||||
|
base_url = "https://cert.api.fiservapps.com/"
|
||||||
|
|
||||||
[connectors.worldpay]
|
[connectors.worldpay]
|
||||||
base_url = "http://localhost:9090/"
|
base_url = "http://localhost:9090/"
|
||||||
|
|
||||||
|
|||||||
@ -133,6 +133,9 @@ base_url = "https://apitest.cybersource.com/"
|
|||||||
[connectors.shift4]
|
[connectors.shift4]
|
||||||
base_url = "https://api.shift4.com/"
|
base_url = "https://api.shift4.com/"
|
||||||
|
|
||||||
|
[connectors.fiserv]
|
||||||
|
base_url = "https://cert.api.fiservapps.com/"
|
||||||
|
|
||||||
[connectors.worldpay]
|
[connectors.worldpay]
|
||||||
base_url = "https://try.access.worldpay.com/"
|
base_url = "https://try.access.worldpay.com/"
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,9 @@ base_url = "https://apitest.cybersource.com/"
|
|||||||
[connectors.shift4]
|
[connectors.shift4]
|
||||||
base_url = "https://api.shift4.com/"
|
base_url = "https://api.shift4.com/"
|
||||||
|
|
||||||
|
[connectors.fiserv]
|
||||||
|
base_url = "https://cert.api.fiservapps.com/"
|
||||||
|
|
||||||
[connectors.worldpay]
|
[connectors.worldpay]
|
||||||
base_url = "https://try.access.worldpay.com/"
|
base_url = "https://try.access.worldpay.com/"
|
||||||
|
|
||||||
@ -93,4 +96,4 @@ base_url = "https://apis.sandbox.globalpay.com/ucp/"
|
|||||||
|
|
||||||
[connectors.supported]
|
[connectors.supported]
|
||||||
wallets = ["klarna", "braintree", "applepay"]
|
wallets = ["klarna", "braintree", "applepay"]
|
||||||
cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "shift4", "cybersource", "worldpay", "globalpay"]
|
cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "shift4", "cybersource", "worldpay", "globalpay", "fiserv"]
|
||||||
|
|||||||
@ -503,6 +503,7 @@ pub enum Connector {
|
|||||||
Cybersource,
|
Cybersource,
|
||||||
#[default]
|
#[default]
|
||||||
Dummy,
|
Dummy,
|
||||||
|
Fiserv,
|
||||||
Globalpay,
|
Globalpay,
|
||||||
Klarna,
|
Klarna,
|
||||||
Payu,
|
Payu,
|
||||||
|
|||||||
@ -27,6 +27,11 @@ pub mod date_time {
|
|||||||
pub fn convert_to_pdt(offset_time: OffsetDateTime) -> PrimitiveDateTime {
|
pub fn convert_to_pdt(offset_time: OffsetDateTime) -> PrimitiveDateTime {
|
||||||
PrimitiveDateTime::new(offset_time.date(), offset_time.time())
|
PrimitiveDateTime::new(offset_time.date(), offset_time.time())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the UNIX timestamp of the current date and time in UTC
|
||||||
|
pub fn now_unix_timestamp() -> i64 {
|
||||||
|
OffsetDateTime::now_utc().unix_timestamp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a nanoid with the given prefix and length
|
/// Generate a nanoid with the given prefix and length
|
||||||
|
|||||||
@ -63,4 +63,4 @@ max_read_count = 100
|
|||||||
|
|
||||||
[connectors.supported]
|
[connectors.supported]
|
||||||
wallets = ["klarna","braintree"]
|
wallets = ["klarna","braintree"]
|
||||||
cards = ["stripe","adyen","authorizedotnet","checkout","braintree", "cybersource"]
|
cards = ["stripe","adyen","authorizedotnet","checkout","braintree", "cybersource", "fiserv"]
|
||||||
|
|||||||
@ -127,6 +127,7 @@ pub struct Connectors {
|
|||||||
pub braintree: ConnectorParams,
|
pub braintree: ConnectorParams,
|
||||||
pub checkout: ConnectorParams,
|
pub checkout: ConnectorParams,
|
||||||
pub cybersource: ConnectorParams,
|
pub cybersource: ConnectorParams,
|
||||||
|
pub fiserv: ConnectorParams,
|
||||||
pub globalpay: ConnectorParams,
|
pub globalpay: ConnectorParams,
|
||||||
pub klarna: ConnectorParams,
|
pub klarna: ConnectorParams,
|
||||||
pub payu: ConnectorParams,
|
pub payu: ConnectorParams,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ pub mod authorizedotnet;
|
|||||||
pub mod braintree;
|
pub mod braintree;
|
||||||
pub mod checkout;
|
pub mod checkout;
|
||||||
pub mod cybersource;
|
pub mod cybersource;
|
||||||
|
pub mod fiserv;
|
||||||
pub mod globalpay;
|
pub mod globalpay;
|
||||||
pub mod klarna;
|
pub mod klarna;
|
||||||
pub mod payu;
|
pub mod payu;
|
||||||
@ -15,6 +16,7 @@ pub mod worldpay;
|
|||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
||||||
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, globalpay::Globalpay,
|
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, fiserv::Fiserv,
|
||||||
klarna::Klarna, payu::Payu, shift4::Shift4, stripe::Stripe, worldpay::Worldpay,
|
globalpay::Globalpay, klarna::Klarna, payu::Payu, shift4::Shift4, stripe::Stripe,
|
||||||
|
worldpay::Worldpay,
|
||||||
};
|
};
|
||||||
|
|||||||
426
crates/router/src/connector/fiserv.rs
Normal file
426
crates/router/src/connector/fiserv.rs
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
mod transformers;
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use error_stack::ResultExt;
|
||||||
|
use ring::hmac;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use transformers as fiserv;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
configs::settings,
|
||||||
|
consts,
|
||||||
|
core::{
|
||||||
|
errors::{self, CustomResult},
|
||||||
|
payments,
|
||||||
|
},
|
||||||
|
headers, services,
|
||||||
|
types::{
|
||||||
|
self,
|
||||||
|
api::{self},
|
||||||
|
},
|
||||||
|
utils::{self, BytesExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Fiserv;
|
||||||
|
|
||||||
|
impl Fiserv {
|
||||||
|
pub fn generate_authorization_signature(
|
||||||
|
&self,
|
||||||
|
auth: fiserv::FiservAuthType,
|
||||||
|
request_id: &str,
|
||||||
|
payload: &str,
|
||||||
|
timestamp: i128,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
let fiserv::FiservAuthType {
|
||||||
|
api_key,
|
||||||
|
api_secret,
|
||||||
|
..
|
||||||
|
} = auth;
|
||||||
|
let raw_signature = format!("{api_key}{request_id}{timestamp}{payload}");
|
||||||
|
|
||||||
|
let key = hmac::Key::new(hmac::HMAC_SHA256, api_secret.as_bytes());
|
||||||
|
let signature_value =
|
||||||
|
consts::BASE64_ENGINE.encode(hmac::sign(&key, raw_signature.as_bytes()).as_ref());
|
||||||
|
Ok(signature_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::ConnectorCommon for Fiserv {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"fiserv"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_get_content_type(&self) -> &'static str {
|
||||||
|
"application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
|
||||||
|
connectors.fiserv.base_url.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::Payment for Fiserv {}
|
||||||
|
|
||||||
|
impl api::PreVerify for Fiserv {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Verify,
|
||||||
|
types::VerifyRequestData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Fiserv
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentVoid for Fiserv {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Void,
|
||||||
|
types::PaymentsCancelData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Fiserv
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentSync for Fiserv {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||||
|
for Fiserv
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentCapture for Fiserv {}
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Capture,
|
||||||
|
types::PaymentsCaptureData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Fiserv
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCaptureRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000;
|
||||||
|
let auth: fiserv::FiservAuthType =
|
||||||
|
fiserv::FiservAuthType::try_from(&req.connector_auth_type)?;
|
||||||
|
let api_key = auth.api_key.clone();
|
||||||
|
|
||||||
|
let fiserv_req = self
|
||||||
|
.get_request_body(req)?
|
||||||
|
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
let client_request_id = Uuid::new_v4().to_string();
|
||||||
|
let hmac = self
|
||||||
|
.generate_authorization_signature(auth, &client_request_id, &fiserv_req, timestamp)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
let headers = vec![
|
||||||
|
(
|
||||||
|
headers::CONTENT_TYPE.to_string(),
|
||||||
|
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
|
||||||
|
),
|
||||||
|
("Client-Request-Id".to_string(), client_request_id),
|
||||||
|
("Auth-Token-Type".to_string(), "HMAC".to_string()),
|
||||||
|
("Api-Key".to_string(), api_key),
|
||||||
|
("Timestamp".to_string(), timestamp.to_string()),
|
||||||
|
("Authorization".to_string(), hmac),
|
||||||
|
];
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
"application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCaptureRouterData,
|
||||||
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
|
let fiserv_req = utils::Encode::<fiserv::FiservCaptureRequest>::convert_and_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Some(fiserv_req))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCaptureRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
let request = Some(
|
||||||
|
services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
|
||||||
|
.headers(types::PaymentsCaptureType::get_headers(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsCaptureRouterData,
|
||||||
|
res: types::Response,
|
||||||
|
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||||
|
let response: fiserv::FiservPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("Fiserv Payment Response")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsCaptureRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{}ch/payments/v1/charges",
|
||||||
|
connectors.fiserv.base_url
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Bytes,
|
||||||
|
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
|
||||||
|
let response: fiserv::ErrorResponse = res
|
||||||
|
.parse_struct("Fiserv ErrorResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
|
||||||
|
let fiserv::ErrorResponse { error, details } = response;
|
||||||
|
|
||||||
|
let message = match (error, details) {
|
||||||
|
(Some(err), _) => err
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.message.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(""),
|
||||||
|
(None, Some(err_details)) => err_details
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.message.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(""),
|
||||||
|
(None, None) => consts::NO_ERROR_MESSAGE.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(types::ErrorResponse {
|
||||||
|
code: consts::NO_ERROR_CODE.to_string(),
|
||||||
|
message,
|
||||||
|
reason: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentSession for Fiserv {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Session,
|
||||||
|
types::PaymentsSessionData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Fiserv
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::PaymentAuthorize for Fiserv {}
|
||||||
|
|
||||||
|
impl
|
||||||
|
services::ConnectorIntegration<
|
||||||
|
api::Authorize,
|
||||||
|
types::PaymentsAuthorizeData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Fiserv
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsAuthorizeRouterData,
|
||||||
|
_connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000;
|
||||||
|
let auth: fiserv::FiservAuthType =
|
||||||
|
fiserv::FiservAuthType::try_from(&req.connector_auth_type)?;
|
||||||
|
let api_key = auth.api_key.clone();
|
||||||
|
|
||||||
|
let fiserv_req = self
|
||||||
|
.get_request_body(req)?
|
||||||
|
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
let client_request_id = Uuid::new_v4().to_string();
|
||||||
|
let hmac = self
|
||||||
|
.generate_authorization_signature(auth, &client_request_id, &fiserv_req, timestamp)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
let headers = vec![
|
||||||
|
(
|
||||||
|
headers::CONTENT_TYPE.to_string(),
|
||||||
|
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
|
||||||
|
),
|
||||||
|
("Client-Request-Id".to_string(), client_request_id),
|
||||||
|
("Auth-Token-Type".to_string(), "HMAC".to_string()),
|
||||||
|
("Api-Key".to_string(), api_key),
|
||||||
|
("Timestamp".to_string(), timestamp.to_string()),
|
||||||
|
("Authorization".to_string(), hmac),
|
||||||
|
];
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
"application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsAuthorizeRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{}ch/payments/v1/charges",
|
||||||
|
connectors.fiserv.base_url
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsAuthorizeRouterData,
|
||||||
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
|
let fiserv_req = utils::Encode::<fiserv::FiservPaymentsRequest>::convert_and_encode(req)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Some(fiserv_req))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsAuthorizeRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
let request = Some(
|
||||||
|
services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.url(&types::PaymentsAuthorizeType::get_url(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.headers(types::PaymentsAuthorizeType::get_headers(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsAuthorizeRouterData,
|
||||||
|
res: types::Response,
|
||||||
|
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||||
|
let response: fiserv::FiservPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("Fiserv PaymentResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Bytes,
|
||||||
|
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
|
||||||
|
let response: fiserv::ErrorResponse = res
|
||||||
|
.parse_struct("Fiserv ErrorResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
|
||||||
|
let fiserv::ErrorResponse { error, details } = response;
|
||||||
|
|
||||||
|
let message = match (error, details) {
|
||||||
|
(Some(err), _) => err
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.message.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(""),
|
||||||
|
(None, Some(err_details)) => err_details
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.message.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(""),
|
||||||
|
(None, None) => consts::NO_ERROR_MESSAGE.to_string(),
|
||||||
|
};
|
||||||
|
Ok(types::ErrorResponse {
|
||||||
|
code: consts::NO_ERROR_CODE.to_string(),
|
||||||
|
message,
|
||||||
|
reason: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api::Refund for Fiserv {}
|
||||||
|
impl api::RefundExecute for Fiserv {}
|
||||||
|
impl api::RefundSync for Fiserv {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
|
||||||
|
for Fiserv
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
|
||||||
|
for Fiserv
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl api::IncomingWebhook for Fiserv {
|
||||||
|
fn get_webhook_object_reference_id(
|
||||||
|
&self,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_event_type(
|
||||||
|
&self,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_webhook_resource_object(
|
||||||
|
&self,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl services::ConnectorRedirectResponse for Fiserv {
|
||||||
|
fn get_flow_type(
|
||||||
|
&self,
|
||||||
|
_query_params: &str,
|
||||||
|
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
|
||||||
|
Ok(payments::CallConnectorAction::Trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
343
crates/router/src/connector/fiserv/transformers.rs
Normal file
343
crates/router/src/connector/fiserv/transformers.rs
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
use common_utils::ext_traits::ValueExt;
|
||||||
|
use error_stack::ResultExt;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
core::errors,
|
||||||
|
pii::{self, Secret},
|
||||||
|
types::{self, api, storage::enums},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FiservPaymentsRequest {
|
||||||
|
amount: Amount,
|
||||||
|
source: Source,
|
||||||
|
transaction_details: TransactionDetails,
|
||||||
|
merchant_details: MerchantDetails,
|
||||||
|
transaction_interaction: TransactionInteraction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Source {
|
||||||
|
source_type: String,
|
||||||
|
card: CardData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CardData {
|
||||||
|
card_data: Secret<String, pii::CardNumber>,
|
||||||
|
expiration_month: Secret<String>,
|
||||||
|
expiration_year: Secret<String>,
|
||||||
|
security_code: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
pub struct Amount {
|
||||||
|
total: i64,
|
||||||
|
currency: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TransactionDetails {
|
||||||
|
capture_flag: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MerchantDetails {
|
||||||
|
merchant_id: String,
|
||||||
|
terminal_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TransactionInteraction {
|
||||||
|
origin: String,
|
||||||
|
eci_indicator: String,
|
||||||
|
pos_condition_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::PaymentsAuthorizeRouterData> for FiservPaymentsRequest {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||||
|
match item.request.payment_method_data {
|
||||||
|
api::PaymentMethod::Card(ref ccard) => {
|
||||||
|
let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?;
|
||||||
|
let amount = Amount {
|
||||||
|
total: item.request.amount,
|
||||||
|
currency: item.request.currency.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let card = CardData {
|
||||||
|
card_data: ccard.card_number.clone(),
|
||||||
|
expiration_month: ccard.card_exp_month.clone(),
|
||||||
|
expiration_year: ccard.card_exp_year.clone(),
|
||||||
|
security_code: ccard.card_cvc.clone(),
|
||||||
|
};
|
||||||
|
let source = Source {
|
||||||
|
source_type: "PaymentCard".to_string(),
|
||||||
|
card,
|
||||||
|
};
|
||||||
|
let transaction_details = TransactionDetails {
|
||||||
|
capture_flag: matches!(
|
||||||
|
item.request.capture_method,
|
||||||
|
Some(enums::CaptureMethod::Automatic) | None
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let metadata = item
|
||||||
|
.connector_meta_data
|
||||||
|
.clone()
|
||||||
|
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
let session: SessionObject = metadata
|
||||||
|
.parse_value("SessionObject")
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
|
||||||
|
let merchant_details = MerchantDetails {
|
||||||
|
merchant_id: auth.merchant_account,
|
||||||
|
terminal_id: session.terminal_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let transaction_interaction = TransactionInteraction {
|
||||||
|
origin: "ECOM".to_string(), //Payment is being made in online mode, card not present
|
||||||
|
eci_indicator: "CHANNEL_ENCRYPTED".to_string(), // transaction encryption such as SSL/TLS, but authentication was not performed
|
||||||
|
pos_condition_code: "CARD_NOT_PRESENT_ECOM".to_string(), //card not present in online transaction
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
amount,
|
||||||
|
source,
|
||||||
|
transaction_details,
|
||||||
|
merchant_details,
|
||||||
|
transaction_interaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(errors::ConnectorError::NotImplemented(
|
||||||
|
"Payment Methods".to_string(),
|
||||||
|
))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FiservAuthType {
|
||||||
|
pub(super) api_key: String,
|
||||||
|
pub(super) merchant_account: String,
|
||||||
|
pub(super) api_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::ConnectorAuthType> for FiservAuthType {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||||
|
if let types::ConnectorAuthType::SignatureKey {
|
||||||
|
api_key,
|
||||||
|
key1,
|
||||||
|
api_secret,
|
||||||
|
} = auth_type
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
api_key: api_key.to_string(),
|
||||||
|
merchant_account: key1.to_string(),
|
||||||
|
api_secret: api_secret.to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(errors::ConnectorError::FailedToObtainAuthType)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub details: Option<Vec<ErrorDetails>>,
|
||||||
|
pub error: Option<Vec<ErrorDetails>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ErrorDetails {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub error_type: String,
|
||||||
|
pub code: Option<String>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum FiservPaymentStatus {
|
||||||
|
Succeeded,
|
||||||
|
Failed,
|
||||||
|
Captured,
|
||||||
|
Declined,
|
||||||
|
Voided,
|
||||||
|
Authorized,
|
||||||
|
#[default]
|
||||||
|
Processing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FiservPaymentStatus> for enums::AttemptStatus {
|
||||||
|
fn from(item: FiservPaymentStatus) -> Self {
|
||||||
|
match item {
|
||||||
|
FiservPaymentStatus::Captured | FiservPaymentStatus::Succeeded => Self::Charged,
|
||||||
|
FiservPaymentStatus::Declined | FiservPaymentStatus::Failed => Self::Failure,
|
||||||
|
FiservPaymentStatus::Processing => Self::Authorizing,
|
||||||
|
FiservPaymentStatus::Voided => Self::Voided,
|
||||||
|
FiservPaymentStatus::Authorized => Self::Authorized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FiservPaymentsResponse {
|
||||||
|
gateway_response: GatewayResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GatewayResponse {
|
||||||
|
gateway_transaction_id: String,
|
||||||
|
transaction_state: FiservPaymentStatus,
|
||||||
|
transaction_processing_details: TransactionProcessingDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TransactionProcessingDetails {
|
||||||
|
order_id: String,
|
||||||
|
transaction_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, T>
|
||||||
|
TryFrom<types::ResponseRouterData<F, FiservPaymentsResponse, T, types::PaymentsResponseData>>
|
||||||
|
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(
|
||||||
|
item: types::ResponseRouterData<F, FiservPaymentsResponse, T, types::PaymentsResponseData>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let gateway_resp = item.response.gateway_response;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
status: gateway_resp.transaction_state.into(),
|
||||||
|
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
|
resource_id: types::ResponseId::ConnectorTransactionId(
|
||||||
|
gateway_resp.transaction_processing_details.transaction_id,
|
||||||
|
),
|
||||||
|
redirection_data: None,
|
||||||
|
redirect: false,
|
||||||
|
mandate_reference: None,
|
||||||
|
connector_metadata: None,
|
||||||
|
}),
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FiservCaptureRequest {
|
||||||
|
amount: Amount,
|
||||||
|
transaction_details: TransactionDetails,
|
||||||
|
merchant_details: MerchantDetails,
|
||||||
|
reference_transaction_details: ReferenceTransactionDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReferenceTransactionDetails {
|
||||||
|
reference_transaction_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SessionObject {
|
||||||
|
pub terminal_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::PaymentsCaptureRouterData> for FiservCaptureRequest {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
|
||||||
|
let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?;
|
||||||
|
let metadata = item
|
||||||
|
.connector_meta_data
|
||||||
|
.clone()
|
||||||
|
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
let session: SessionObject = metadata
|
||||||
|
.parse_value("SessionObject")
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
let amount = item
|
||||||
|
.request
|
||||||
|
.amount_to_capture
|
||||||
|
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Self {
|
||||||
|
amount: Amount {
|
||||||
|
total: amount,
|
||||||
|
currency: item.request.currency.to_string(),
|
||||||
|
},
|
||||||
|
transaction_details: TransactionDetails { capture_flag: true },
|
||||||
|
merchant_details: MerchantDetails {
|
||||||
|
merchant_id: auth.merchant_account,
|
||||||
|
terminal_id: session.terminal_id,
|
||||||
|
},
|
||||||
|
reference_transaction_details: ReferenceTransactionDetails {
|
||||||
|
reference_transaction_id: item.request.connector_transaction_id.to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize)]
|
||||||
|
pub struct FiservRefundRequest {}
|
||||||
|
|
||||||
|
impl<F> TryFrom<&types::RefundsRouterData<F>> for FiservRefundRequest {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(_item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
|
||||||
|
pub enum RefundStatus {
|
||||||
|
Succeeded,
|
||||||
|
Failed,
|
||||||
|
#[default]
|
||||||
|
Processing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RefundStatus> for enums::RefundStatus {
|
||||||
|
fn from(item: RefundStatus) -> Self {
|
||||||
|
match item {
|
||||||
|
RefundStatus::Succeeded => Self::Success,
|
||||||
|
RefundStatus::Failed => Self::Failure,
|
||||||
|
RefundStatus::Processing => Self::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RefundResponse {}
|
||||||
|
|
||||||
|
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||||
|
for types::RefundsRouterData<api::Execute>
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(
|
||||||
|
_item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
|
||||||
|
for types::RefundsRouterData<api::RSync>
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(
|
||||||
|
_item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -455,11 +455,11 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsCaptureData {
|
|||||||
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
|
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
amount_to_capture: payment_data.payment_attempt.amount_to_capture,
|
amount_to_capture: payment_data.payment_attempt.amount_to_capture,
|
||||||
|
currency: payment_data.currency,
|
||||||
connector_transaction_id: payment_data
|
connector_transaction_id: payment_data
|
||||||
.payment_attempt
|
.payment_attempt
|
||||||
.connector_transaction_id
|
.connector_transaction_id
|
||||||
.ok_or(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)?,
|
.ok_or(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)?,
|
||||||
currency: payment_data.currency,
|
|
||||||
amount: payment_data.amount.into(),
|
amount: payment_data.amount.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,8 +112,8 @@ pub struct PaymentsAuthorizeData {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PaymentsCaptureData {
|
pub struct PaymentsCaptureData {
|
||||||
pub amount_to_capture: Option<i64>,
|
pub amount_to_capture: Option<i64>,
|
||||||
pub connector_transaction_id: String,
|
|
||||||
pub currency: storage_enums::Currency,
|
pub currency: storage_enums::Currency,
|
||||||
|
pub connector_transaction_id: String,
|
||||||
pub amount: i64,
|
pub amount: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -139,19 +139,20 @@ impl ConnectorData {
|
|||||||
connector_name: &str,
|
connector_name: &str,
|
||||||
) -> CustomResult<BoxedConnector, errors::ApiErrorResponse> {
|
) -> CustomResult<BoxedConnector, errors::ApiErrorResponse> {
|
||||||
match connector_name {
|
match connector_name {
|
||||||
"stripe" => Ok(Box::new(&connector::Stripe)),
|
|
||||||
"adyen" => Ok(Box::new(&connector::Adyen)),
|
|
||||||
"aci" => Ok(Box::new(&connector::Aci)),
|
"aci" => Ok(Box::new(&connector::Aci)),
|
||||||
"checkout" => Ok(Box::new(&connector::Checkout)),
|
"adyen" => Ok(Box::new(&connector::Adyen)),
|
||||||
|
"applepay" => Ok(Box::new(&connector::Applepay)),
|
||||||
"authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)),
|
"authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)),
|
||||||
"braintree" => Ok(Box::new(&connector::Braintree)),
|
"braintree" => Ok(Box::new(&connector::Braintree)),
|
||||||
"klarna" => Ok(Box::new(&connector::Klarna)),
|
"checkout" => Ok(Box::new(&connector::Checkout)),
|
||||||
"applepay" => Ok(Box::new(&connector::Applepay)),
|
|
||||||
"cybersource" => Ok(Box::new(&connector::Cybersource)),
|
"cybersource" => Ok(Box::new(&connector::Cybersource)),
|
||||||
"shift4" => Ok(Box::new(&connector::Shift4)),
|
"fiserv" => Ok(Box::new(&connector::Fiserv)),
|
||||||
"worldpay" => Ok(Box::new(&connector::Worldpay)),
|
|
||||||
"payu" => Ok(Box::new(&connector::Payu)),
|
|
||||||
"globalpay" => Ok(Box::new(&connector::Globalpay)),
|
"globalpay" => Ok(Box::new(&connector::Globalpay)),
|
||||||
|
"klarna" => Ok(Box::new(&connector::Klarna)),
|
||||||
|
"payu" => Ok(Box::new(&connector::Payu)),
|
||||||
|
"shift4" => Ok(Box::new(&connector::Shift4)),
|
||||||
|
"stripe" => Ok(Box::new(&connector::Stripe)),
|
||||||
|
"worldpay" => Ok(Box::new(&connector::Worldpay)),
|
||||||
_ => Err(report!(errors::ConnectorError::InvalidConnectorName)
|
_ => Err(report!(errors::ConnectorError::InvalidConnectorName)
|
||||||
.attach_printable(format!("invalid connector name: {connector_name}")))
|
.attach_printable(format!("invalid connector name: {connector_name}")))
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError),
|
.change_context(errors::ApiErrorResponse::InternalServerError),
|
||||||
|
|||||||
@ -6,6 +6,7 @@ pub(crate) struct ConnectorAuthentication {
|
|||||||
pub aci: Option<BodyKey>,
|
pub aci: Option<BodyKey>,
|
||||||
pub authorizedotnet: Option<BodyKey>,
|
pub authorizedotnet: Option<BodyKey>,
|
||||||
pub checkout: Option<BodyKey>,
|
pub checkout: Option<BodyKey>,
|
||||||
|
pub fiserv: Option<SignatureKey>,
|
||||||
pub globalpay: Option<HeaderKey>,
|
pub globalpay: Option<HeaderKey>,
|
||||||
pub payu: Option<BodyKey>,
|
pub payu: Option<BodyKey>,
|
||||||
pub shift4: Option<HeaderKey>,
|
pub shift4: Option<HeaderKey>,
|
||||||
@ -50,3 +51,20 @@ impl From<BodyKey> for ConnectorAuthType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub(crate) struct SignatureKey {
|
||||||
|
pub api_key: String,
|
||||||
|
pub key1: String,
|
||||||
|
pub api_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SignatureKey> for ConnectorAuthType {
|
||||||
|
fn from(key: SignatureKey) -> Self {
|
||||||
|
Self::SignatureKey {
|
||||||
|
api_key: key.api_key,
|
||||||
|
key1: key.key1,
|
||||||
|
api_secret: key.api_secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
129
crates/router/tests/connectors/fiserv.rs
Normal file
129
crates/router/tests/connectors/fiserv.rs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
use masking::Secret;
|
||||||
|
use router::types::{self, api, storage::enums};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
connector_auth,
|
||||||
|
utils::{self, ConnectorActions},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Fiserv;
|
||||||
|
impl ConnectorActions for Fiserv {}
|
||||||
|
|
||||||
|
impl utils::Connector for Fiserv {
|
||||||
|
fn get_data(&self) -> types::api::ConnectorData {
|
||||||
|
use router::connector::Fiserv;
|
||||||
|
types::api::ConnectorData {
|
||||||
|
connector: Box::new(&Fiserv),
|
||||||
|
connector_name: types::Connector::Fiserv,
|
||||||
|
get_token: types::api::GetToken::Connector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_auth_token(&self) -> types::ConnectorAuthType {
|
||||||
|
types::ConnectorAuthType::from(
|
||||||
|
connector_auth::ConnectorAuthentication::new()
|
||||||
|
.fiserv
|
||||||
|
.expect("Missing connector authentication configuration"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name(&self) -> String {
|
||||||
|
"fiserv".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_connector_meta(&self) -> Option<serde_json::Value> {
|
||||||
|
Some(json!({"terminalId": "10000001"}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_only_authorize_payment() {
|
||||||
|
let response = Fiserv {}
|
||||||
|
.authorize_payment(
|
||||||
|
Some(types::PaymentsAuthorizeData {
|
||||||
|
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
|
||||||
|
card_number: Secret::new("4005550000000019".to_string()),
|
||||||
|
card_exp_month: Secret::new("02".to_string()),
|
||||||
|
card_exp_year: Secret::new("2035".to_string()),
|
||||||
|
card_holder_name: Secret::new("John Doe".to_string()),
|
||||||
|
card_cvc: Secret::new("123".to_string()),
|
||||||
|
}),
|
||||||
|
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
|
||||||
|
..utils::PaymentAuthorizeType::default().0
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_authorize_and_capture_payment() {
|
||||||
|
let response = Fiserv {}
|
||||||
|
.make_payment(
|
||||||
|
Some(types::PaymentsAuthorizeData {
|
||||||
|
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
|
||||||
|
card_number: Secret::new("4005550000000019".to_string()),
|
||||||
|
card_exp_month: Secret::new("02".to_string()),
|
||||||
|
card_exp_year: Secret::new("2035".to_string()),
|
||||||
|
card_holder_name: Secret::new("John Doe".to_string()),
|
||||||
|
card_cvc: Secret::new("123".to_string()),
|
||||||
|
}),
|
||||||
|
..utils::PaymentAuthorizeType::default().0
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// You get a service declined for Payment Capture, look into it from merchant dashboard
|
||||||
|
/*
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_capture_already_authorized_payment() {
|
||||||
|
let connector = Fiserv {};
|
||||||
|
let authorize_response = connector.authorize_payment(None, None).await;
|
||||||
|
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
|
||||||
|
let txn_id = utils::get_connector_transaction_id(authorize_response);
|
||||||
|
let response: OptionFuture<_> = txn_id
|
||||||
|
.map(|transaction_id| async move {
|
||||||
|
connector.capture_payment(transaction_id, None, None).await.status
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
assert_eq!(response.await, Some(enums::AttemptStatus::Charged));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_fail_payment_for_incorrect_cvc() {
|
||||||
|
let response = Fiserv {}.make_payment(Some(types::PaymentsAuthorizeData {
|
||||||
|
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
|
||||||
|
card_number: Secret::new("4024007134364842".to_string()),
|
||||||
|
..utils::CCardType::default().0
|
||||||
|
}),
|
||||||
|
..utils::PaymentAuthorizeType::default().0
|
||||||
|
}), None)
|
||||||
|
.await;
|
||||||
|
let x = response.response.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
x.message,
|
||||||
|
"The card's security code failed verification.".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn should_refund_succeeded_payment() {
|
||||||
|
let connector = Fiserv {};
|
||||||
|
//make a successful payment
|
||||||
|
let response = connector.make_payment(None, None).await;
|
||||||
|
|
||||||
|
//try refund for previous payment
|
||||||
|
if let Some(transaction_id) = utils::get_connector_transaction_id(response) {
|
||||||
|
let response = connector.refund_payment(transaction_id, None, None).await;
|
||||||
|
assert_eq!(
|
||||||
|
response.response.unwrap().refund_status,
|
||||||
|
enums::RefundStatus::Success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@ -4,6 +4,7 @@ mod aci;
|
|||||||
mod authorizedotnet;
|
mod authorizedotnet;
|
||||||
mod checkout;
|
mod checkout;
|
||||||
mod connector_auth;
|
mod connector_auth;
|
||||||
|
mod fiserv;
|
||||||
mod globalpay;
|
mod globalpay;
|
||||||
mod payu;
|
mod payu;
|
||||||
mod shift4;
|
mod shift4;
|
||||||
|
|||||||
@ -25,3 +25,9 @@ key1 = "MerchantPosId"
|
|||||||
|
|
||||||
[globalpay]
|
[globalpay]
|
||||||
api_key = "Bearer MyApiKey"
|
api_key = "Bearer MyApiKey"
|
||||||
|
|
||||||
|
[fiserv]
|
||||||
|
api_key = "MyApiKey"
|
||||||
|
key1 = "MerchantID"
|
||||||
|
api_secret = "MySecretKey"
|
||||||
|
|
||||||
|
|||||||
@ -78,8 +78,8 @@ pub trait ConnectorActions: Connector {
|
|||||||
let request = self.generate_data(
|
let request = self.generate_data(
|
||||||
payment_data.unwrap_or(types::PaymentsCaptureData {
|
payment_data.unwrap_or(types::PaymentsCaptureData {
|
||||||
amount_to_capture: Some(100),
|
amount_to_capture: Some(100),
|
||||||
connector_transaction_id: transaction_id,
|
|
||||||
currency: enums::Currency::USD,
|
currency: enums::Currency::USD,
|
||||||
|
connector_transaction_id: transaction_id,
|
||||||
amount: 100,
|
amount: 100,
|
||||||
}),
|
}),
|
||||||
payment_info,
|
payment_info,
|
||||||
|
|||||||
Reference in New Issue
Block a user