mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +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]
|
||||
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]
|
||||
max_attempts = 10
|
||||
@ -82,6 +82,9 @@ base_url = "https://apitest.cybersource.com/"
|
||||
[connectors.shift4]
|
||||
base_url = "https://api.shift4.com/"
|
||||
|
||||
[connectors.fiserv]
|
||||
base_url = "https://cert.api.fiservapps.com/"
|
||||
|
||||
[connectors.worldpay]
|
||||
base_url = "http://localhost:9090/"
|
||||
|
||||
|
||||
@ -133,6 +133,9 @@ base_url = "https://apitest.cybersource.com/"
|
||||
[connectors.shift4]
|
||||
base_url = "https://api.shift4.com/"
|
||||
|
||||
[connectors.fiserv]
|
||||
base_url = "https://cert.api.fiservapps.com/"
|
||||
|
||||
[connectors.worldpay]
|
||||
base_url = "https://try.access.worldpay.com/"
|
||||
|
||||
|
||||
@ -85,6 +85,9 @@ base_url = "https://apitest.cybersource.com/"
|
||||
[connectors.shift4]
|
||||
base_url = "https://api.shift4.com/"
|
||||
|
||||
[connectors.fiserv]
|
||||
base_url = "https://cert.api.fiservapps.com/"
|
||||
|
||||
[connectors.worldpay]
|
||||
base_url = "https://try.access.worldpay.com/"
|
||||
|
||||
@ -93,4 +96,4 @@ base_url = "https://apis.sandbox.globalpay.com/ucp/"
|
||||
|
||||
[connectors.supported]
|
||||
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,
|
||||
#[default]
|
||||
Dummy,
|
||||
Fiserv,
|
||||
Globalpay,
|
||||
Klarna,
|
||||
Payu,
|
||||
|
||||
@ -27,6 +27,11 @@ pub mod date_time {
|
||||
pub fn convert_to_pdt(offset_time: OffsetDateTime) -> PrimitiveDateTime {
|
||||
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
|
||||
|
||||
@ -63,4 +63,4 @@ max_read_count = 100
|
||||
|
||||
[connectors.supported]
|
||||
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 checkout: ConnectorParams,
|
||||
pub cybersource: ConnectorParams,
|
||||
pub fiserv: ConnectorParams,
|
||||
pub globalpay: ConnectorParams,
|
||||
pub klarna: ConnectorParams,
|
||||
pub payu: ConnectorParams,
|
||||
|
||||
@ -5,6 +5,7 @@ pub mod authorizedotnet;
|
||||
pub mod braintree;
|
||||
pub mod checkout;
|
||||
pub mod cybersource;
|
||||
pub mod fiserv;
|
||||
pub mod globalpay;
|
||||
pub mod klarna;
|
||||
pub mod payu;
|
||||
@ -15,6 +16,7 @@ pub mod worldpay;
|
||||
|
||||
pub use self::{
|
||||
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
||||
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, globalpay::Globalpay,
|
||||
klarna::Klarna, payu::Payu, shift4::Shift4, stripe::Stripe, worldpay::Worldpay,
|
||||
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, fiserv::Fiserv,
|
||||
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> {
|
||||
Ok(Self {
|
||||
amount_to_capture: payment_data.payment_attempt.amount_to_capture,
|
||||
currency: payment_data.currency,
|
||||
connector_transaction_id: payment_data
|
||||
.payment_attempt
|
||||
.connector_transaction_id
|
||||
.ok_or(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)?,
|
||||
currency: payment_data.currency,
|
||||
amount: payment_data.amount.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -112,8 +112,8 @@ pub struct PaymentsAuthorizeData {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PaymentsCaptureData {
|
||||
pub amount_to_capture: Option<i64>,
|
||||
pub connector_transaction_id: String,
|
||||
pub currency: storage_enums::Currency,
|
||||
pub connector_transaction_id: String,
|
||||
pub amount: i64,
|
||||
}
|
||||
|
||||
|
||||
@ -139,19 +139,20 @@ impl ConnectorData {
|
||||
connector_name: &str,
|
||||
) -> CustomResult<BoxedConnector, errors::ApiErrorResponse> {
|
||||
match connector_name {
|
||||
"stripe" => Ok(Box::new(&connector::Stripe)),
|
||||
"adyen" => Ok(Box::new(&connector::Adyen)),
|
||||
"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)),
|
||||
"braintree" => Ok(Box::new(&connector::Braintree)),
|
||||
"klarna" => Ok(Box::new(&connector::Klarna)),
|
||||
"applepay" => Ok(Box::new(&connector::Applepay)),
|
||||
"checkout" => Ok(Box::new(&connector::Checkout)),
|
||||
"cybersource" => Ok(Box::new(&connector::Cybersource)),
|
||||
"shift4" => Ok(Box::new(&connector::Shift4)),
|
||||
"worldpay" => Ok(Box::new(&connector::Worldpay)),
|
||||
"payu" => Ok(Box::new(&connector::Payu)),
|
||||
"fiserv" => Ok(Box::new(&connector::Fiserv)),
|
||||
"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)
|
||||
.attach_printable(format!("invalid connector name: {connector_name}")))
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError),
|
||||
|
||||
@ -6,6 +6,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub aci: Option<BodyKey>,
|
||||
pub authorizedotnet: Option<BodyKey>,
|
||||
pub checkout: Option<BodyKey>,
|
||||
pub fiserv: Option<SignatureKey>,
|
||||
pub globalpay: Option<HeaderKey>,
|
||||
pub payu: Option<BodyKey>,
|
||||
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 checkout;
|
||||
mod connector_auth;
|
||||
mod fiserv;
|
||||
mod globalpay;
|
||||
mod payu;
|
||||
mod shift4;
|
||||
|
||||
@ -25,3 +25,9 @@ key1 = "MerchantPosId"
|
||||
|
||||
[globalpay]
|
||||
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(
|
||||
payment_data.unwrap_or(types::PaymentsCaptureData {
|
||||
amount_to_capture: Some(100),
|
||||
connector_transaction_id: transaction_id,
|
||||
currency: enums::Currency::USD,
|
||||
connector_transaction_id: transaction_id,
|
||||
amount: 100,
|
||||
}),
|
||||
payment_info,
|
||||
|
||||
Reference in New Issue
Block a user