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