feat(connector): [Fiserv] add Refunds, Cancel and Wallets flow along with Unit Tests (#593)

Co-authored-by: samraat bansal <samraat.bansal@samraat.bansal-MacBookPro>
Co-authored-by: Nishant Joshi <nishant.joshi@juspay.in>
Co-authored-by: Jagan <jaganelavarasan@gmail.com>
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
SamraatBansal
2023-03-09 17:00:10 +05:30
committed by GitHub
parent df8c8b5aa4
commit cd1c540906
8 changed files with 1196 additions and 351 deletions

View File

@ -155,10 +155,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for AirwallexPaymentsCaptureRequ
Ok(Self {
request_id: Uuid::new_v4().to_string(),
amount: match item.request.amount_to_capture {
Some(_a) => Some(utils::to_currency_base_unit(
item.request.amount,
item.request.currency,
)?),
Some(a) => Some(utils::to_currency_base_unit(a, item.request.currency)?),
_ => None,
},
})

View File

@ -16,10 +16,11 @@ use crate::{
errors::{self, CustomResult},
payments,
},
headers, services,
headers, logger,
services::{self, api::ConnectorIntegration},
types::{
self,
api::{self},
api::{self, ConnectorCommon, ConnectorCommonExt},
},
utils::{self, BytesExt},
};
@ -49,7 +50,44 @@ impl Fiserv {
}
}
impl api::ConnectorCommon for Fiserv {
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Fiserv
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
_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 mut auth_header = self.get_auth_header(&req.connector_auth_type)?;
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 mut 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()),
(headers::TIMESTAMP.to_string(), timestamp.to_string()),
(headers::AUTHORIZATION.to_string(), hmac),
];
headers.append(&mut auth_header);
Ok(headers)
}
}
impl ConnectorCommon for Fiserv {
fn id(&self) -> &'static str {
"fiserv"
}
@ -61,16 +99,54 @@ impl api::ConnectorCommon for Fiserv {
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.fiserv.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth: fiserv::FiservAuthType = auth_type
.try_into()
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(headers::API_KEY.to_string(), auth.api_key)])
}
fn build_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: fiserv::ErrorResponse = res
.response
.parse_struct("Fiserv ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let fiserv::ErrorResponse { error, details } = response;
Ok(error
.or(details)
.and_then(|error_details| {
error_details
.first()
.map(|first_error| types::ErrorResponse {
code: first_error
.code
.to_owned()
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: first_error.message.to_owned(),
reason: first_error.field.to_owned(),
status_code: res.status_code,
})
})
.unwrap_or(types::ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: consts::NO_ERROR_MESSAGE.to_string(),
reason: None,
status_code: res.status_code,
}))
}
}
impl api::ConnectorAccessToken for Fiserv {}
impl
services::ConnectorIntegration<
api::AccessTokenAuth,
types::AccessTokenRequestData,
types::AccessToken,
> for Fiserv
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
for Fiserv
{
// Not Implemented (R)
}
@ -80,73 +156,188 @@ 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 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 ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Fiserv
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
"application/json"
}
fn get_url(
&self,
_req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
//The docs has this url wrong, cancels is the working endpoint
"{}ch/payments/v1/cancels",
connectors.fiserv.base_url
))
}
fn get_request_body(
&self,
req: &types::PaymentsCancelRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = fiserv::FiservCancelRequest::try_from(req)?;
let fiserv_req =
utils::Encode::<fiserv::FiservCancelRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(fiserv_req))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.body(types::PaymentsVoidType::get_request_body(self, req)?)
.build(),
);
Ok(request)
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsCancelRouterData, 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: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentSync for Fiserv {}
#[allow(dead_code)]
impl
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Fiserv
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
"application/json"
}
fn get_url(
&self,
_req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}ch/payments/v1/transaction-inquiry",
connectors.fiserv.base_url
))
}
fn get_request_body(
&self,
req: &types::PaymentsSyncRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = fiserv::FiservSyncRequest::try_from(req)?;
let fiserv_req =
utils::Encode::<fiserv::FiservSyncRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(fiserv_req))
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.body(types::PaymentsSyncType::get_request_body(self, req)?)
.build(),
);
Ok(request)
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: fiserv::FiservSyncResponse = res
.response
.parse_struct("Fiserv PaymentSyncResponse")
.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: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentCapture for Fiserv {}
impl
services::ConnectorIntegration<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
> for Fiserv
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Fiserv
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
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)
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
@ -215,86 +406,29 @@ impl
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: fiserv::ErrorResponse = res
.response
.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 {
status_code: res.status_code,
code: consts::NO_ERROR_CODE.to_string(),
message,
reason: None,
})
self.build_error_response(res)
}
}
impl api::PaymentSession for Fiserv {}
#[allow(dead_code)]
impl
services::ConnectorIntegration<
api::Session,
types::PaymentsSessionData,
types::PaymentsResponseData,
> for Fiserv
impl 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
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Fiserv
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
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)
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
@ -367,32 +501,7 @@ impl
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: fiserv::ErrorResponse = res
.response
.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 {
status_code: res.status_code,
code: consts::NO_ERROR_CODE.to_string(),
message,
reason: None,
})
self.build_error_response(res)
}
}
@ -401,15 +510,157 @@ impl api::RefundExecute for Fiserv {}
impl api::RefundSync for Fiserv {}
#[allow(dead_code)]
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Fiserv
{
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Fiserv {
fn get_headers(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
"application/json"
}
fn get_url(
&self,
_req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}ch/payments/v1/refunds",
connectors.fiserv.base_url
))
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = fiserv::FiservRefundRequest::try_from(req)?;
let fiserv_req =
utils::Encode::<fiserv::FiservRefundRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(fiserv_req))
}
fn build_request(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
.headers(types::RefundExecuteType::get_headers(
self, req, connectors,
)?)
.body(types::RefundExecuteType::get_request_body(self, req)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::RefundsRouterData<api::Execute>,
res: types::Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
logger::debug!(target: "router::connector::fiserv", response=?res);
let response: fiserv::RefundResponse =
res.response
.parse_struct("fiserv RefundResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[allow(dead_code)]
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
for Fiserv
{
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Fiserv {
fn get_headers(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
"application/json"
}
fn get_url(
&self,
_req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}ch/payments/v1/transaction-inquiry",
connectors.fiserv.base_url
))
}
fn get_request_body(
&self,
req: &types::RefundSyncRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = fiserv::FiservSyncRequest::try_from(req)?;
let fiserv_req =
utils::Encode::<fiserv::FiservSyncRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(fiserv_req))
}
fn build_request(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
.body(types::RefundSyncType::get_request_body(self, req)?)
.build(),
);
Ok(request)
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
res: types::Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
logger::debug!(target: "router::connector::fiserv", response=?res);
let response: fiserv::FiservSyncResponse = res
.response
.parse_struct("Fiserv Refund 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_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[async_trait::async_trait]

View File

@ -3,12 +3,13 @@ use error_stack::ResultExt;
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::{self, PaymentsCancelRequestData, PaymentsSyncRequestData, RouterData},
core::errors,
pii::{self, Secret},
types::{self, api, storage::enums},
};
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[derive(Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FiservPaymentsRequest {
amount: Amount,
@ -18,11 +19,18 @@ pub struct FiservPaymentsRequest {
transaction_interaction: TransactionInteraction,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Source {
source_type: String,
card: CardData,
#[derive(Debug, Serialize, Eq, PartialEq)]
#[serde(tag = "sourceType")]
pub enum Source {
PaymentCard {
card: CardData,
},
#[allow(dead_code)]
GooglePay {
data: String,
signature: String,
version: String,
},
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
@ -34,90 +42,119 @@ pub struct CardData {
security_code: Secret<String>,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GooglePayToken {
signature: String,
signed_message: String,
protocol_version: String,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct Amount {
total: i64,
#[serde(serialize_with = "utils::str_to_f32")]
total: String,
currency: String,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TransactionDetails {
capture_flag: bool,
capture_flag: Option<bool>,
reversal_reason_code: Option<String>,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MerchantDetails {
merchant_id: String,
terminal_id: String,
terminal_id: Option<String>,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TransactionInteraction {
origin: String,
eci_indicator: String,
pos_condition_code: String,
origin: TransactionInteractionOrigin,
eci_indicator: TransactionInteractionEciIndicator,
pos_condition_code: TransactionInteractionPosConditionCode,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum TransactionInteractionOrigin {
#[default]
Ecom,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TransactionInteractionEciIndicator {
#[default]
ChannelEncrypted,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TransactionInteractionPosConditionCode {
#[default]
CardNotPresentEcom,
}
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::PaymentMethodData::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 auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?;
let amount = Amount {
total: utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
currency: item.request.currency.to_string(),
};
let transaction_details = TransactionDetails {
capture_flag: Some(matches!(
item.request.capture_method,
Some(enums::CaptureMethod::Automatic) | None
)),
reversal_reason_code: None,
};
let metadata = item.get_connector_meta()?;
let session: SessionObject = metadata
.parse_value("SessionObject")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let merchant_details = MerchantDetails {
merchant_id: auth.merchant_account,
terminal_id: Some(session.terminal_id),
};
let transaction_interaction = TransactionInteraction {
//Payment is being made in online mode, card not present
origin: TransactionInteractionOrigin::Ecom,
// transaction encryption such as SSL/TLS, but authentication was not performed
eci_indicator: TransactionInteractionEciIndicator::ChannelEncrypted,
//card not present in online transaction
pos_condition_code: TransactionInteractionPosConditionCode::CardNotPresentEcom,
};
let source = match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(ref ccard) => {
let card = CardData {
card_data: ccard.card_number.clone(),
card_data: ccard
.card_number
.clone()
.map(|card| card.split_whitespace().collect()),
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,
})
Source::PaymentCard { card }
}
_ => Err(errors::ConnectorError::NotImplemented(
"Payment Methods".to_string(),
))?,
}
};
Ok(Self {
amount,
source,
transaction_details,
merchant_details,
transaction_interaction,
})
}
}
@ -147,6 +184,38 @@ impl TryFrom<&types::ConnectorAuthType> for FiservAuthType {
}
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FiservCancelRequest {
transaction_details: TransactionDetails,
merchant_details: MerchantDetails,
reference_transaction_details: ReferenceTransactionDetails,
}
impl TryFrom<&types::PaymentsCancelRouterData> for FiservCancelRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?;
let metadata = item.get_connector_meta()?;
let session: SessionObject = metadata
.parse_value("SessionObject")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Self {
merchant_details: MerchantDetails {
merchant_id: auth.merchant_account,
terminal_id: Some(session.terminal_id),
},
reference_transaction_details: ReferenceTransactionDetails {
reference_transaction_id: item.request.connector_transaction_id.to_string(),
},
transaction_details: TransactionDetails {
capture_flag: None,
reversal_reason_code: Some(item.request.get_cancellation_reason()?),
},
})
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorResponse {
@ -161,6 +230,7 @@ pub struct ErrorDetails {
pub error_type: String,
pub code: Option<String>,
pub message: String,
pub field: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
@ -188,12 +258,31 @@ impl From<FiservPaymentStatus> for enums::AttemptStatus {
}
}
impl From<FiservPaymentStatus> for enums::RefundStatus {
fn from(item: FiservPaymentStatus) -> Self {
match item {
FiservPaymentStatus::Succeeded
| FiservPaymentStatus::Authorized
| FiservPaymentStatus::Captured => Self::Success,
FiservPaymentStatus::Declined | FiservPaymentStatus::Failed => Self::Failure,
_ => Self::Pending,
}
}
}
#[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")]
#[serde(transparent)]
pub struct FiservSyncResponse {
sync_responses: Vec<FiservPaymentsResponse>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GatewayResponse {
@ -220,7 +309,7 @@ impl<F, T>
let gateway_resp = item.response.gateway_response;
Ok(Self {
status: gateway_resp.transaction_state.into(),
status: enums::AttemptStatus::from(gateway_resp.transaction_state),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
gateway_resp.transaction_processing_details.transaction_id,
@ -234,6 +323,39 @@ impl<F, T>
}
}
impl<F, T> TryFrom<types::ResponseRouterData<F, FiservSyncResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, FiservSyncResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let gateway_resp = match item.response.sync_responses.first() {
Some(gateway_response) => gateway_response,
_ => Err(errors::ConnectorError::ResponseHandlingFailed)?,
};
Ok(Self {
status: enums::AttemptStatus::from(
gateway_resp.gateway_response.transaction_state.clone(),
),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
gateway_resp
.gateway_response
.transaction_processing_details
.transaction_id
.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
}
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FiservCaptureRequest {
@ -266,19 +388,22 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for FiservCaptureRequest {
let session: SessionObject = metadata
.parse_value("SessionObject")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let amount = item
.request
.amount_to_capture
.ok_or(errors::ConnectorError::RequestEncodingFailed)?;
let amount = match item.request.amount_to_capture {
Some(a) => utils::to_currency_base_unit(a, item.request.currency)?,
_ => utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
};
Ok(Self {
amount: Amount {
total: amount,
currency: item.request.currency.to_string(),
},
transaction_details: TransactionDetails { capture_flag: true },
transaction_details: TransactionDetails {
capture_flag: Some(true),
reversal_reason_code: None,
},
merchant_details: MerchantDetails {
merchant_id: auth.merchant_account,
terminal_id: session.terminal_id,
terminal_id: Some(session.terminal_id),
},
reference_transaction_details: ReferenceTransactionDetails {
reference_transaction_id: item.request.connector_transaction_id.to_string(),
@ -287,56 +412,143 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for FiservCaptureRequest {
}
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FiservSyncRequest {
merchant_details: MerchantDetails,
reference_transaction_details: ReferenceTransactionDetails,
}
impl TryFrom<&types::PaymentsSyncRouterData> for FiservSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsSyncRouterData) -> Result<Self, Self::Error> {
let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?;
Ok(Self {
merchant_details: MerchantDetails {
merchant_id: auth.merchant_account,
terminal_id: None,
},
reference_transaction_details: ReferenceTransactionDetails {
reference_transaction_id: item
.request
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?,
},
})
}
}
impl TryFrom<&types::RefundSyncRouterData> for FiservSyncRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundSyncRouterData) -> Result<Self, Self::Error> {
let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?;
Ok(Self {
merchant_details: MerchantDetails {
merchant_id: auth.merchant_account,
terminal_id: None,
},
reference_transaction_details: ReferenceTransactionDetails {
reference_transaction_id: item
.request
.connector_refund_id
.clone()
.ok_or(errors::ConnectorError::RequestEncodingFailed)?,
},
})
}
}
#[derive(Default, Debug, Serialize)]
pub struct FiservRefundRequest {}
#[serde(rename_all = "camelCase")]
pub struct FiservRefundRequest {
amount: Amount,
merchant_details: MerchantDetails,
reference_transaction_details: ReferenceTransactionDetails,
}
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())
fn try_from(item: &types::RefundsRouterData<F>) -> 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)?;
Ok(Self {
amount: Amount {
total: utils::to_currency_base_unit(
item.request.refund_amount,
item.request.currency,
)?,
currency: item.request.currency.to_string(),
},
merchant_details: MerchantDetails {
merchant_id: auth.merchant_account,
terminal_id: Some(session.terminal_id),
},
reference_transaction_details: ReferenceTransactionDetails {
reference_transaction_id: item.request.connector_transaction_id.to_string(),
},
})
}
}
#[allow(dead_code)]
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RefundResponse {
gateway_response: GatewayResponse,
}
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>,
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
) -> Result<Self, Self::Error> {
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item
.response
.gateway_response
.transaction_processing_details
.transaction_id,
refund_status: enums::RefundStatus::from(
item.response.gateway_response.transaction_state,
),
}),
..item.data
})
}
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
impl TryFrom<types::RefundsResponseRouterData<api::RSync, FiservSyncResponse>>
for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
_item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
item: types::RefundsResponseRouterData<api::RSync, FiservSyncResponse>,
) -> Result<Self, Self::Error> {
Err(errors::ConnectorError::NotImplemented("fiserv".to_string()).into())
let gateway_resp = item
.response
.sync_responses
.first()
.ok_or(errors::ConnectorError::ResponseHandlingFailed)?;
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: gateway_resp
.gateway_response
.transaction_processing_details
.transaction_id
.clone(),
refund_status: enums::RefundStatus::from(
gateway_resp.gateway_response.transaction_state.clone(),
),
}),
..item.data
})
}
}

View File

@ -5,11 +5,12 @@ use error_stack::{report, IntoReport, ResultExt};
use masking::Secret;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Serializer;
use crate::{
core::errors::{self, CustomResult},
pii::PeekInterface,
types::{self, api, PaymentsCancelData},
types::{self, api, PaymentsCancelData, ResponseId},
utils::OptionExt,
};
@ -142,17 +143,29 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
pub trait PaymentsSyncRequestData {
fn is_auto_capture(&self) -> bool;
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError>;
}
impl PaymentsSyncRequestData for types::PaymentsSyncData {
fn is_auto_capture(&self) -> bool {
self.capture_method == Some(storage_models::enums::CaptureMethod::Automatic)
}
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError> {
match self.connector_transaction_id.clone() {
ResponseId::ConnectorTransactionId(txn_id) => Ok(txn_id),
_ => Err(errors::ValidationError::IncorrectValueProvided {
field_name: "connector_transaction_id",
})
.into_report()
.attach_printable("Expected connector transaction ID not found"),
}
}
}
pub trait PaymentsCancelRequestData {
fn get_amount(&self) -> Result<i64, Error>;
fn get_currency(&self) -> Result<storage_models::enums::Currency, Error>;
fn get_cancellation_reason(&self) -> Result<String, Error>;
}
impl PaymentsCancelRequestData for PaymentsCancelData {
@ -162,6 +175,11 @@ impl PaymentsCancelRequestData for PaymentsCancelData {
fn get_currency(&self) -> Result<storage_models::enums::Currency, Error> {
self.currency.ok_or_else(missing_field_err("currency"))
}
fn get_cancellation_reason(&self) -> Result<String, Error> {
self.cancellation_reason
.clone()
.ok_or_else(missing_field_err("cancellation_reason"))
}
}
pub trait RefundsRequestData {
@ -355,3 +373,13 @@ pub fn to_currency_base_unit(
_ => Ok((f64::from(amount_u32) / 100.0).to_string()),
}
}
pub fn str_to_f32<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let float_value = value.parse::<f64>().map_err(|_| {
serde::ser::Error::custom("Invalid string, cannot be converted to float value")
})?;
serializer.serialize_f64(float_value)
}

View File

@ -278,6 +278,8 @@ pub enum ConnectorError {
WebhookResourceObjectNotFound,
#[error("Invalid Date/time format")]
InvalidDateFormat,
#[error("Invalid Data format")]
InvalidDataFormat { field_name: &'static str },
#[error("Payment Method data / Payment Method Type / Payment Experience Mismatch ")]
MismatchedPaymentData,
}

View File

@ -64,7 +64,7 @@ pub async fn construct_refund_router_data<'a, F>(
// Does refund need shipping/billing address ?
address: PaymentAddress::default(),
auth_type: payment_attempt.authentication_type.unwrap_or_default(),
connector_meta_data: None,
connector_meta_data: merchant_connector_account.metadata,
amount_captured: payment_intent.amount_captured,
request: types::RefundsData {
refund_id: refund.refund_id.clone(),

View File

@ -42,14 +42,16 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
/// Header Constants
pub mod headers {
pub const X_API_KEY: &str = "X-API-KEY";
pub const CONTENT_TYPE: &str = "Content-Type";
pub const X_ROUTER: &str = "X-router";
pub const AUTHORIZATION: &str = "Authorization";
pub const ACCEPT: &str = "Accept";
pub const X_API_VERSION: &str = "X-ApiVersion";
pub const API_KEY: &str = "API-KEY";
pub const AUTHORIZATION: &str = "Authorization";
pub const CONTENT_TYPE: &str = "Content-Type";
pub const DATE: &str = "Date";
pub const TIMESTAMP: &str = "Timestamp";
pub const X_API_KEY: &str = "X-API-KEY";
pub const X_API_VERSION: &str = "X-ApiVersion";
pub const X_MERCHANT_ID: &str = "X-Merchant-Id";
pub const X_ROUTER: &str = "X-router";
pub const X_LOGIN: &str = "X-Login";
pub const X_TRANS_KEY: &str = "X-Trans-Key";
pub const X_VERSION: &str = "X-Version";