feat(connector): implement authorize and capture flows for Fiserv (#266)

This commit is contained in:
Sahebjot singh
2023-01-15 16:03:06 +05:30
committed by GitHub
parent f0b89dda7b
commit 6e15f5a990
18 changed files with 958 additions and 16 deletions

View File

@ -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/"

View File

@ -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/"

View File

@ -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"]

View File

@ -503,6 +503,7 @@ pub enum Connector {
Cybersource,
#[default]
Dummy,
Fiserv,
Globalpay,
Klarna,
Payu,

View File

@ -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

View File

@ -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"]

View File

@ -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,

View File

@ -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,
};

View 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)
}
}

View 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())
}
}

View File

@ -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(),
})
}

View File

@ -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,
}

View File

@ -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),

View File

@ -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,
}
}
}

View 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,
);
}
}
*/

View File

@ -4,6 +4,7 @@ mod aci;
mod authorizedotnet;
mod checkout;
mod connector_auth;
mod fiserv;
mod globalpay;
mod payu;
mod shift4;

View File

@ -25,3 +25,9 @@ key1 = "MerchantPosId"
[globalpay]
api_key = "Bearer MyApiKey"
[fiserv]
api_key = "MyApiKey"
key1 = "MerchantID"
api_secret = "MySecretKey"

View File

@ -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,