diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index d93208fb66..53af0100c4 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -7629,6 +7629,7 @@ "fiservemea", "fiuu", "forte", + "getnet", "globalpay", "globepay", "gocardless", @@ -9757,6 +9758,12 @@ "user_card_cvc" ] }, + { + "type": "string", + "enum": [ + "user_card_network" + ] + }, { "type": "string", "enum": [ @@ -20297,6 +20304,7 @@ "fiservemea", "fiuu", "forte", + "getnet", "globalpay", "globepay", "gocardless", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index c2fe52010f..cc1207d477 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9728,6 +9728,7 @@ "fiservemea", "fiuu", "forte", + "getnet", "globalpay", "globepay", "gocardless", @@ -11971,6 +11972,12 @@ "user_card_cvc" ] }, + { + "type": "string", + "enum": [ + "user_card_network" + ] + }, { "type": "string", "enum": [ @@ -24783,6 +24790,7 @@ "fiservemea", "fiuu", "forte", + "getnet", "globalpay", "globepay", "gocardless", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 6ae46a5ede..bd328c75a0 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -211,6 +211,7 @@ pub enum FieldType { UserCardExpiryMonth, UserCardExpiryYear, UserCardCvc, + UserCardNetwork, UserFullName, UserEmailAddress, UserPhoneNumber, diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index cc86e6897f..5def313fd1 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -81,7 +81,7 @@ pub enum RoutableConnectors { Fiservemea, Fiuu, Forte, - // Getnet, + Getnet, Globalpay, Globepay, Gocardless, @@ -224,7 +224,7 @@ pub enum Connector { Fiservemea, Fiuu, Forte, - // Getnet, + Getnet, Globalpay, Globepay, Gocardless, @@ -382,7 +382,7 @@ impl Connector { | Self::Fiservemea | Self::Fiuu | Self::Forte - // | Self::Getnet + | Self::Getnet | Self::Globalpay | Self::Globepay | Self::Gocardless @@ -521,6 +521,7 @@ impl From for Connector { RoutableConnectors::Fiservemea => Self::Fiservemea, RoutableConnectors::Fiuu => Self::Fiuu, RoutableConnectors::Forte => Self::Forte, + RoutableConnectors::Getnet => Self::Getnet, RoutableConnectors::Globalpay => Self::Globalpay, RoutableConnectors::Globepay => Self::Globepay, RoutableConnectors::Gocardless => Self::Gocardless, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index baa9dd1319..ae617c5930 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -195,7 +195,7 @@ pub struct ConnectorConfig { pub fiservemea: Option, pub fiuu: Option, pub forte: Option, - // pub getnet: Option, + pub getnet: Option, pub globalpay: Option, pub globepay: Option, pub gocardless: Option, @@ -367,7 +367,7 @@ impl ConnectorConfig { Connector::Fiservemea => Ok(connector_data.fiservemea), Connector::Fiuu => Ok(connector_data.fiuu), Connector::Forte => Ok(connector_data.forte), - // Connector::Getnet => Ok(connector_data.getnet), + Connector::Getnet => Ok(connector_data.getnet), Connector::Globalpay => Ok(connector_data.globalpay), Connector::Globepay => Ok(connector_data.globepay), Connector::Gocardless => Ok(connector_data.gocardless), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index a1dd0cac47..e166e2795c 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1667,6 +1667,30 @@ key2="Location ID" [forte.connector_webhook_details] merchant_secret="Source verification key" +[getnet] +[[getnet.credit]] + payment_method_type = "Mastercard" +[[getnet.credit]] + payment_method_type = "Visa" +[[getnet.credit]] + payment_method_type = "Interac" +[[getnet.credit]] + payment_method_type = "AmericanExpress" +[[getnet.credit]] + payment_method_type = "JCB" +[[getnet.credit]] + payment_method_type = "DinersClub" +[[getnet.credit]] + payment_method_type = "Discover" +[[getnet.credit]] + payment_method_type = "CartesBancaires" +[[getnet.credit]] + payment_method_type = "UnionPay" +[[getnet.credit]] + payment_method_type = "Rupay" +[[getnet.credit]] + payment_method_type = "Maestro" + [globalpay] [[globalpay.credit]] payment_method_type = "Mastercard" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index aada50faa6..1555814a5e 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1385,6 +1385,30 @@ key2="Location ID" merchant_secret="Source verification key" +[getnet] +[[getnet.credit]] + payment_method_type = "Mastercard" +[[getnet.credit]] + payment_method_type = "Visa" +[[getnet.credit]] + payment_method_type = "Interac" +[[getnet.credit]] + payment_method_type = "AmericanExpress" +[[getnet.credit]] + payment_method_type = "JCB" +[[getnet.credit]] + payment_method_type = "DinersClub" +[[getnet.credit]] + payment_method_type = "Discover" +[[getnet.credit]] + payment_method_type = "CartesBancaires" +[[getnet.credit]] + payment_method_type = "UnionPay" +[[getnet.credit]] + payment_method_type = "Rupay" +[[getnet.credit]] + payment_method_type = "Maestro" + [globalpay] [[globalpay.credit]] payment_method_type = "Mastercard" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 6bf33b902f..45fc6d51ee 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1617,6 +1617,30 @@ key2="Location ID" [forte.connector_webhook_details] merchant_secret="Source verification key" +[getnet] +[[getnet.credit]] + payment_method_type = "Mastercard" +[[getnet.credit]] + payment_method_type = "Visa" +[[getnet.credit]] + payment_method_type = "Interac" +[[getnet.credit]] + payment_method_type = "AmericanExpress" +[[getnet.credit]] + payment_method_type = "JCB" +[[getnet.credit]] + payment_method_type = "DinersClub" +[[getnet.credit]] + payment_method_type = "Discover" +[[getnet.credit]] + payment_method_type = "CartesBancaires" +[[getnet.credit]] + payment_method_type = "UnionPay" +[[getnet.credit]] + payment_method_type = "Rupay" +[[getnet.credit]] + payment_method_type = "Maestro" + [globalpay] [[globalpay.credit]] payment_method_type = "Mastercard" diff --git a/crates/hyperswitch_connectors/src/connectors/getnet.rs b/crates/hyperswitch_connectors/src/connectors/getnet.rs index 557dfbeaca..263b1ad5b4 100644 --- a/crates/hyperswitch_connectors/src/connectors/getnet.rs +++ b/crates/hyperswitch_connectors/src/connectors/getnet.rs @@ -1,12 +1,16 @@ pub mod transformers; - +use api_models::webhooks::IncomingWebhookEvent; +use base64::{self, Engine}; +use common_enums::enums; use common_utils::{ + consts::BASE64_ENGINE, + crypto, errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, FloatMajorUnit, FloatMajorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ @@ -21,8 +25,8 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, }, }; use hyperswitch_interfaces::{ @@ -31,25 +35,26 @@ use hyperswitch_interfaces::{ ConnectorValidation, }, configs::Connectors, - errors, + errors::{self}, events::connector_api_logs::ConnectorEvent, types::{self, Response}, - webhooks, + webhooks::{self}, }; -use masking::{ExposeInterface, Mask}; +use masking::{Mask, PeekInterface, Secret}; +use ring::hmac; use transformers as getnet; use crate::{constants::headers, types::ResponseRouterData, utils}; #[derive(Clone)] pub struct Getnet { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Getnet { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &FloatMajorUnitForConnector, } } } @@ -82,10 +87,16 @@ where req: &RouterData, _connectors: &Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - self.get_content_type().to_string().into(), - )]; + let mut header = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + headers::ACCEPT.to_string(), + self.get_accept_type().to_string().into(), + ), + ]; let mut api_key = self.get_auth_header(&req.connector_auth_type)?; header.append(&mut api_key); Ok(header) @@ -99,9 +110,6 @@ impl ConnectorCommon for Getnet { fn get_currency_unit(&self) -> api::CurrencyUnit { api::CurrencyUnit::Base - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { @@ -118,9 +126,12 @@ impl ConnectorCommon for Getnet { ) -> CustomResult)>, errors::ConnectorError> { let auth = getnet::GetnetAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let encoded_api_key = + BASE64_ENGINE.encode(format!("{}:{}", auth.username.peek(), auth.password.peek())); + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + format!("Basic {encoded_api_key}").into_masked(), )]) } @@ -149,16 +160,60 @@ impl ConnectorCommon for Getnet { } impl ConnectorValidation for Getnet { - //TODO: implement functions when support enabled + fn validate_connector_against_payment_request( + &self, + capture_method: Option, + _payment_method: enums::PaymentMethod, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic + | enums::CaptureMethod::Manual + | enums::CaptureMethod::SequentialAutomatic => Ok(()), + enums::CaptureMethod::Scheduled | enums::CaptureMethod::ManualMultiple => Err( + utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } + + fn validate_psync_reference_id( + &self, + data: &PaymentsSyncData, + _is_three_ds: bool, + _status: enums::AttemptStatus, + _connector_meta_data: Option, + ) -> CustomResult<(), errors::ConnectorError> { + if data.encoded_data.is_some() + || data + .connector_transaction_id + .get_connector_transaction_id() + .is_ok() + { + return Ok(()); + } + + Err(errors::ConnectorError::MissingConnectorTransactionID.into()) + } } -impl ConnectorIntegration for Getnet { - //TODO: implement sessions flow -} +impl ConnectorIntegration for Getnet {} impl ConnectorIntegration for Getnet {} -impl ConnectorIntegration for Getnet {} +impl ConnectorIntegration for Getnet { + // Not Implemented (R) + fn build_request( + &self, + _req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Getnet".to_string()) + .into(), + ) + } +} impl ConnectorIntegration for Getnet { fn get_headers( @@ -176,9 +231,10 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + Ok(format!("{endpoint}/payments/")) } fn get_request_body( @@ -191,10 +247,11 @@ impl ConnectorIntegration for Get fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let auth = getnet::GetnetAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let merchant_id = auth.merchant_id.peek(); + + let endpoint = self.base_url(connectors); + let transaction_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + + Ok(format!( + "{}/merchants/{}/payments/{}", + endpoint, merchant_id, transaction_id + )) } fn build_request( @@ -327,17 +398,26 @@ impl ConnectorIntegration fo fn get_url( &self, _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + Ok(format!("{endpoint}/payments/")) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &PaymentsCaptureRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + + let connector_router_data = getnet::GetnetRouterData::from((amount, req)); + let connector_req = getnet::GetnetCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -366,7 +446,7 @@ impl ConnectorIntegration fo event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: getnet::GetnetPaymentsResponse = res + let response: getnet::GetnetCaptureResponse = res .response .parse_struct("Getnet PaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -377,6 +457,86 @@ impl ConnectorIntegration fo data: data.clone(), http_code: res.status_code, }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Getnet { + fn get_headers( + &self, + req: &PaymentsCancelRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsCancelRouterData, + connectors: &Connectors, + ) -> CustomResult { + let endpoint = self.base_url(connectors); + Ok(format!("{endpoint}/payments/")) + } + + fn get_request_body( + &self, + req: &PaymentsCancelRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = getnet::GetnetCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsCancelRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCancelRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: getnet::GetnetCancelResponse = res + .response + .parse_struct("GetnetPaymentsVoidResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } fn get_error_response( @@ -388,8 +548,6 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Getnet {} - impl ConnectorIntegration for Getnet { fn get_headers( &self, @@ -406,9 +564,10 @@ impl ConnectorIntegration for Getnet fn get_url( &self, _req: &RefundsRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let endpoint = self.base_url(connectors); + Ok(format!("{endpoint}/payments/")) } fn get_request_body( @@ -489,10 +648,19 @@ impl ConnectorIntegration for Getnet { fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let auth = getnet::GetnetAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let merchant_id = auth.merchant_id.peek(); + let endpoint = self.base_url(connectors); + let transaction_id = req.request.connector_transaction_id.clone(); + + Ok(format!( + "{}/merchants/{}/payments/{}", + endpoint, merchant_id, transaction_id + )) } fn build_request( @@ -543,25 +711,118 @@ impl ConnectorIntegration for Getnet { #[async_trait::async_trait] impl webhooks::IncomingWebhook for Getnet { - fn get_webhook_object_reference_id( + fn get_webhook_source_verification_algorithm( &self, _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let notif_item = getnet::get_webhook_response(request.body) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let response_base64 = ¬if_item.response_base64.peek().clone(); + BASE64_ENGINE + .decode(response_base64) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + } + + fn get_webhook_source_verification_message( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _merchant_id: &common_utils::id_type::MerchantId, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let notif = getnet::get_webhook_response(request.body) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + Ok(notif.response_base64.peek().clone().into_bytes()) + } + + fn get_webhook_object_reference_id( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let notif = getnet::get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + let transaction_type = ¬if.payment.transaction_type; + if getnet::is_refund_event(transaction_type) { + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId( + notif.payment.transaction_id.to_string(), + ), + )) + } else { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + notif.payment.transaction_id.to_string(), + ), + )) + } } fn get_webhook_event_type( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let notif = getnet::get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + let incoming_webhook_event = getnet::get_incoming_webhook_event( + notif.payment.transaction_type, + notif.payment.transaction_state, + ); + Ok(incoming_webhook_event) } fn get_webhook_resource_object( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let notif = getnet::get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + Ok(Box::new(notif)) + } + + async fn verify_webhook_source( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + merchant_id: &common_utils::id_type::MerchantId, + connector_webhook_details: Option, + _connector_account_details: crypto::Encryptable>, + connector_name: &str, + ) -> CustomResult { + let notif_item = getnet::get_webhook_response(request.body) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let connector_webhook_secrets = self + .get_webhook_source_verification_merchant_secret( + merchant_id, + connector_name, + connector_webhook_details, + ) + .await?; + + let signature = notif_item.response_signature_base64.peek().clone(); + + let message = self.get_webhook_source_verification_message( + request, + merchant_id, + &connector_webhook_secrets, + )?; + + let secret = connector_webhook_secrets.secret; + + let key = hmac::Key::new(hmac::HMAC_SHA256, &secret); + let result = hmac::sign(&key, &message); + + let computed_signature = BASE64_ENGINE.encode(result.as_ref()); + + let normalized_computed_signature = computed_signature.replace("+", " "); + Ok(signature == normalized_computed_signature) } } diff --git a/crates/hyperswitch_connectors/src/connectors/getnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/getnet/transformers.rs index a6dd52d83f..31aef0a4b1 100644 --- a/crates/hyperswitch_connectors/src/connectors/getnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/getnet/transformers.rs @@ -1,31 +1,47 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use api_models::webhooks::IncomingWebhookEvent; +use base64::Engine; +use cards::CardNumber; +use common_enums::{enums, AttemptStatus, CaptureMethod, CountryAlpha2}; +use common_utils::{ + consts::BASE64_ENGINE, + errors::CustomResult, + pii::{Email, IpAddress}, + types::FloatMajorUnit, +}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, router_data::{ConnectorAuthType, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, + router_request_types::{ + PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, ResponseId, + }, router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundsRouterData, + }, }; use hyperswitch_interfaces::errors; -use masking::Secret; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + connectors::paybox::transformers::parse_url_encoded_to_struct, + types::{PaymentsSyncResponseRouterData, RefundsResponseRouterData, ResponseRouterData}, + utils::{ + BrowserInformationData, PaymentsAuthorizeRequestData, PaymentsSyncRequestData, + RouterData as _, + }, }; -//TODO: Fill the struct with respective fields pub struct GetnetRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: FloatMajorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for GetnetRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(FloatMajorUnit, T)> for GetnetRouterData { + fn from((amount, item): (FloatMajorUnit, T)) -> Self { Self { amount, router_data: item, @@ -33,39 +49,226 @@ impl From<(StringMinorUnit, T)> for GetnetRouterData { } } -//TODO: Fill the struct with respective fields +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Amount { + pub value: FloatMajorUnit, + pub currency: enums::Currency, +} +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Address { + #[serde(rename = "street1")] + pub street1: Option>, + pub city: Option, + pub state: Option>, + pub country: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AccountHolder { + #[serde(rename = "first-name")] + pub first_name: Option>, + #[serde(rename = "last-name")] + pub last_name: Option>, + pub email: Option, + pub phone: Option>, + pub address: Option
, +} + #[derive(Default, Debug, Serialize, PartialEq)] +pub struct Card { + #[serde(rename = "account-number")] + pub account_number: CardNumber, + #[serde(rename = "expiration-month")] + pub expiration_month: Secret, + #[serde(rename = "expiration-year")] + pub expiration_year: Secret, + #[serde(rename = "card-security-code")] + pub card_security_code: Secret, + #[serde(rename = "card-type")] + pub card_type: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum GetnetPaymentMethods { + CreditCard, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PaymentMethod { + pub name: GetnetPaymentMethods, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Notification { + pub url: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PaymentMethodContainer { + #[serde(rename = "payment-method")] + pub payment_method: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum NotificationFormat { + #[serde(rename = "application/json-signed")] + JsonSigned, + #[serde(rename = "application/json")] + Json, + #[serde(rename = "application/xml")] + Xml, + #[serde(rename = "application/html")] + Html, + #[serde(rename = "application/x-www-form-urlencoded")] + Urlencoded, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct NotificationContainer { + pub notification: Vec, + pub format: NotificationFormat, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct MerchantAccountId { + pub value: Secret, +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct PaymentData { + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "requested-amount")] + pub requested_amount: Amount, + #[serde(rename = "account-holder")] + pub account_holder: Option, + pub card: Card, + #[serde(rename = "ip-address")] + pub ip_address: Option>, + #[serde(rename = "payment-methods")] + pub payment_methods: PaymentMethodContainer, + pub notifications: Option, +} + +#[derive(Debug, Serialize)] pub struct GetnetPaymentsRequest { - amount: StringMinorUnit, - card: GetnetCard, + payment: PaymentData, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct GetnetCard { - number: cards::CardNumber, + number: CardNumber, expiry_month: Secret, expiry_year: Secret, cvc: Secret, complete: bool, } +impl TryFrom for PaymentMethodContainer { + type Error = error_stack::Report; + + fn try_from(payment_method_type: enums::PaymentMethodType) -> Result { + match payment_method_type { + enums::PaymentMethodType::Credit => Ok(Self { + payment_method: vec![PaymentMethod { + name: GetnetPaymentMethods::CreditCard, + }], + }), + _ => Err(errors::ConnectorError::NotSupported { + message: "Payment method type not supported".to_string(), + connector: "Getnet", + } + .into()), + } + } +} + impl TryFrom<&GetnetRouterData<&PaymentsAuthorizeRouterData>> for GetnetPaymentsRequest { type Error = error_stack::Report; fn try_from( item: &GetnetRouterData<&PaymentsAuthorizeRouterData>, ) -> Result { match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = GetnetCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, + PaymentMethodData::Card(ref req_card) => { + if item.router_data.is_three_ds() { + return Err(errors::ConnectorError::NotSupported { + message: "3DS payments".to_string(), + connector: "Getnet", + } + .into()); + } + let request = &item.router_data.request; + let auth_type = GetnetAuthType::try_from(&item.router_data.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let merchant_account_id = MerchantAccountId { + value: auth_type.merchant_id, }; - Ok(Self { - amount: item.amount.clone(), + + let requested_amount = Amount { + value: item.amount, + currency: request.currency, + }; + + let account_holder = AccountHolder { + first_name: item.router_data.get_optional_billing_first_name(), + last_name: item.router_data.get_optional_billing_last_name(), + email: item.router_data.request.get_optional_email(), + phone: item.router_data.get_optional_billing_phone_number(), + address: Some(Address { + street1: item.router_data.get_optional_billing_line2(), + city: item.router_data.get_optional_billing_city(), + state: item.router_data.get_optional_billing_state(), + country: item.router_data.get_optional_billing_country(), + }), + }; + + let card = Card { + account_number: req_card.card_number.clone(), + expiration_month: req_card.card_exp_month.clone(), + expiration_year: req_card.card_exp_year.clone(), + card_security_code: req_card.card_cvc.clone(), + card_type: req_card + .card_network + .as_ref() + .map(|network| network.to_string().to_lowercase()) + .unwrap_or_default(), + }; + + let pmt = item.router_data.request.get_payment_method_type()?; + let payment_method = PaymentMethodContainer::try_from(pmt)?; + + let notifications: NotificationContainer = NotificationContainer { + format: NotificationFormat::JsonSigned, + + notification: vec![Notification { + url: Some(item.router_data.request.get_webhook_url()?), + }], + }; + let transaction_type = if request.is_auto_capture()? { + GetnetTransactionType::Purchase + } else { + GetnetTransactionType::Authorization + }; + let payment_data = PaymentData { + merchant_account_id, + request_id: item.router_data.payment_id.clone(), + transaction_type, + requested_amount, + account_holder: Some(account_holder), card, + ip_address: Some(request.get_browser_info()?.get_ip_address()?), + payment_methods: payment_method, + notifications: Some(notifications), + }; + + Ok(Self { + payment: payment_data, }) } _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), @@ -73,62 +276,382 @@ impl TryFrom<&GetnetRouterData<&PaymentsAuthorizeRouterData>> for GetnetPayments } } -//TODO: Fill the struct with respective fields -// Auth Struct pub struct GetnetAuthType { - pub(super) api_key: Secret, + pub username: Secret, + pub password: Secret, + pub merchant_id: Secret, } impl TryFrom<&ConnectorAuthType> for GetnetAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self { + username: key1.to_owned(), + password: api_key.to_owned(), + merchant_id: api_secret.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum GetnetPaymentStatus { - Succeeded, + Success, Failed, #[default] - Processing, + InProgress, } -impl From for common_enums::AttemptStatus { +impl From for AttemptStatus { fn from(item: GetnetPaymentStatus) -> Self { match item { - GetnetPaymentStatus::Succeeded => Self::Charged, + GetnetPaymentStatus::Success => Self::Charged, GetnetPaymentStatus::Failed => Self::Failure, - GetnetPaymentStatus::Processing => Self::Authorizing, + GetnetPaymentStatus::InProgress => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct GetnetPaymentsResponse { - status: GetnetPaymentStatus, - id: String, +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Status { + pub code: String, + pub description: String, + pub severity: String, } -impl TryFrom> - for RouterData +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Statuses { + pub status: Vec, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct CardToken { + #[serde(rename = "token-id")] + pub token_id: Secret, + #[serde(rename = "masked-account-number")] + pub masked_account_number: Secret, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PaymentResponseData { + pub statuses: Statuses, + pub descriptor: Option, + pub notifications: NotificationContainer, + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "transaction-id")] + pub transaction_id: String, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "transaction-state")] + pub transaction_state: GetnetPaymentStatus, + #[serde(rename = "completion-time-stamp")] + pub completion_time_stamp: Option, + #[serde(rename = "requested-amount")] + pub requested_amount: Amount, + #[serde(rename = "account-holder")] + pub account_holder: Option, + #[serde(rename = "card-token")] + pub card_token: CardToken, + #[serde(rename = "ip-address")] + pub ip_address: Option>, + #[serde(rename = "payment-methods")] + pub payment_methods: PaymentMethodContainer, + #[serde(rename = "api-id")] + pub api_id: String, + #[serde(rename = "self")] + pub self_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentsResponse { + payment: PaymentResponseData, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetnetPaymentsResponse { + PaymentsResponse(Box), + GetnetWebhookNotificationResponse(Box), +} + +pub fn authorization_attempt_status_from_transaction_state( + getnet_status: GetnetPaymentStatus, + is_auto_capture: bool, +) -> AttemptStatus { + match getnet_status { + GetnetPaymentStatus::Success => { + if is_auto_capture { + AttemptStatus::Charged + } else { + AttemptStatus::Authorized + } + } + GetnetPaymentStatus::InProgress => AttemptStatus::Pending, + GetnetPaymentStatus::Failed => AttemptStatus::Failure, + } +} + +impl + TryFrom< + ResponseRouterData, + > for RouterData { type Error = error_stack::Report; fn try_from( - item: ResponseRouterData, + item: ResponseRouterData< + F, + GetnetPaymentsResponse, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + ) -> Result { + match item.response { + GetnetPaymentsResponse::PaymentsResponse(ref payment_response) => Ok(Self { + status: authorization_attempt_status_from_transaction_state( + payment_response.payment.transaction_state.clone(), + item.data.request.is_auto_capture()?, + ), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + payment_response.payment.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ..item.data + }), + + _ => Err(error_stack::Report::new( + errors::ConnectorError::ResponseHandlingFailed, + )), + } + } +} + +pub fn psync_attempt_status_from_transaction_state( + getnet_status: GetnetPaymentStatus, + is_auto_capture: bool, + transaction_type: GetnetTransactionType, +) -> AttemptStatus { + match getnet_status { + GetnetPaymentStatus::Success => { + if is_auto_capture && transaction_type == GetnetTransactionType::CaptureAuthorization { + AttemptStatus::Charged + } else { + AttemptStatus::Authorized + } + } + GetnetPaymentStatus::InProgress => AttemptStatus::Pending, + GetnetPaymentStatus::Failed => AttemptStatus::Failure, + } +} + +impl TryFrom> for PaymentsSyncRouterData { + type Error = error_stack::Report; + fn try_from( + item: PaymentsSyncResponseRouterData, + ) -> Result { + match item.response { + GetnetPaymentsResponse::PaymentsResponse(ref payment_response) => Ok(Self { + status: authorization_attempt_status_from_transaction_state( + payment_response.payment.transaction_state.clone(), + item.data.request.is_auto_capture()?, + ), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + payment_response.payment.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ..item.data + }), + + GetnetPaymentsResponse::GetnetWebhookNotificationResponse(ref webhook_response) => { + Ok(Self { + status: psync_attempt_status_from_transaction_state( + webhook_response.payment.transaction_state.clone(), + item.data.request.is_auto_capture()?, + webhook_response.payment.transaction_type.clone(), + ), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + webhook_response.payment.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, + }), + ..item.data + }) + } + } + } +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct CapturePaymentData { + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "parent-transaction-id")] + pub parent_transaction_id: String, + #[serde(rename = "requested-amount")] + pub requested_amount: Amount, + pub notifications: NotificationContainer, + #[serde(rename = "ip-address")] + pub ip_address: Option>, +} + +#[derive(Debug, Serialize)] +pub struct GetnetCaptureRequest { + pub payment: CapturePaymentData, +} +impl TryFrom<&GetnetRouterData<&PaymentsCaptureRouterData>> for GetnetCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &GetnetRouterData<&PaymentsCaptureRouterData>) -> Result { + let request = &item.router_data.request; + let auth_type = GetnetAuthType::try_from(&item.router_data.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let merchant_account_id = MerchantAccountId { + value: auth_type.merchant_id, + }; + + let requested_amount = Amount { + value: item.amount, + currency: request.currency, + }; + let req = &item.router_data.request; + let webhook_url = &req.webhook_url; + let notifications = NotificationContainer { + format: NotificationFormat::JsonSigned, + + notification: vec![Notification { + url: webhook_url.clone(), + }], + }; + let transaction_type = GetnetTransactionType::CaptureAuthorization; + let ip_address = req + .browser_info + .as_ref() + .and_then(|info| info.ip_address.as_ref()) + .map(|ip| Secret::new(ip.to_string())); + let request_id = item.router_data.connector_request_reference_id.clone(); + let parent_transaction_id = item.router_data.request.connector_transaction_id.clone(); + let capture_payment_data = CapturePaymentData { + merchant_account_id, + request_id, + transaction_type, + parent_transaction_id, + requested_amount, + notifications, + ip_address, + }; + + Ok(Self { + payment: capture_payment_data, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CaptureResponseData { + pub statuses: Statuses, + pub descriptor: String, + pub notifications: NotificationContainer, + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "transaction-id")] + pub transaction_id: String, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "transaction-state")] + pub transaction_state: GetnetPaymentStatus, + #[serde(rename = "completion-time-stamp")] + pub completion_time_stamp: Option, + #[serde(rename = "requested-amount")] + pub requested_amount: Amount, + #[serde(rename = "parent-transaction-id")] + pub parent_transaction_id: String, + #[serde(rename = "account-holder")] + pub account_holder: Option, + #[serde(rename = "card-token")] + pub card_token: CardToken, + #[serde(rename = "ip-address")] + pub ip_address: Option>, + #[serde(rename = "payment-methods")] + pub payment_methods: PaymentMethodContainer, + #[serde(rename = "parent-transaction-amount")] + pub parent_transaction_amount: Amount, + #[serde(rename = "authorization-code")] + pub authorization_code: String, + #[serde(rename = "api-id")] + pub api_id: String, + #[serde(rename = "self")] + pub self_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetnetCaptureResponse { + payment: CaptureResponseData, +} + +pub fn capture_status_from_transaction_state(getnet_status: GetnetPaymentStatus) -> AttemptStatus { + match getnet_status { + GetnetPaymentStatus::Success => AttemptStatus::Charged, + GetnetPaymentStatus::InProgress => AttemptStatus::Pending, + GetnetPaymentStatus::Failed => AttemptStatus::Authorized, + } +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + GetnetCaptureResponse, + PaymentsCaptureData, + PaymentsResponseData, + >, ) -> Result { Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), + status: capture_status_from_transaction_state(item.response.payment.transaction_state), response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), + resource_id: ResponseId::ConnectorTransactionId( + item.response.payment.transaction_id, + ), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, @@ -142,50 +665,140 @@ impl TryFrom>, +} +#[derive(Debug, Serialize)] pub struct GetnetRefundRequest { - pub amount: StringMinorUnit, + pub payment: RefundPaymentData, } impl TryFrom<&GetnetRouterData<&RefundsRouterData>> for GetnetRefundRequest { type Error = error_stack::Report; fn try_from(item: &GetnetRouterData<&RefundsRouterData>) -> Result { + let request = &item.router_data.request; + let auth_type = GetnetAuthType::try_from(&item.router_data.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let url = request.webhook_url.clone(); + + let merchant_account_id = MerchantAccountId { + value: auth_type.merchant_id, + }; + let notifications = NotificationContainer { + format: NotificationFormat::JsonSigned, + notification: vec![Notification { url }], + }; + let capture_method = request.capture_method; + let transaction_type = match capture_method { + Some(CaptureMethod::Automatic) => GetnetTransactionType::RefundPurchase, + Some(CaptureMethod::Manual) => GetnetTransactionType::RefundCapture, + Some(CaptureMethod::ManualMultiple) + | Some(CaptureMethod::Scheduled) + | Some(CaptureMethod::SequentialAutomatic) + | None => { + return Err(errors::ConnectorError::CaptureMethodNotSupported {}.into()); + } + }; + let ip_address = request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.ip_address.as_ref()) + .map(|ip| Secret::new(ip.to_string())); + let request_id = item + .router_data + .refund_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorRefundID)?; + + let parent_transaction_id = item.router_data.request.connector_transaction_id.clone(); + let refund_payment_data = RefundPaymentData { + merchant_account_id, + request_id, + transaction_type, + parent_transaction_id, + notifications, + ip_address, + }; + Ok(Self { - amount: item.amount.to_owned(), + payment: refund_payment_data, }) } } -// Type definition for Refund Response - #[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] +#[derive(Debug, Serialize, Default, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] pub enum RefundStatus { - Succeeded, + Success, Failed, #[default] - Processing, + InProgress, } impl From for enums::RefundStatus { fn from(item: RefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, + RefundStatus::Success => Self::Success, RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + RefundStatus::InProgress => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct RefundResponseData { + pub statuses: Statuses, + pub descriptor: String, + pub notifications: NotificationContainer, + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "transaction-id")] + pub transaction_id: String, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "transaction-state")] + pub transaction_state: RefundStatus, + #[serde(rename = "completion-time-stamp")] + pub completion_time_stamp: Option, + #[serde(rename = "requested-amount")] + pub requested_amount: Amount, + #[serde(rename = "parent-transaction-id")] + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_transaction_id: Option, + #[serde(rename = "account-holder")] + pub account_holder: Option, + #[serde(rename = "card-token")] + pub card_token: CardToken, + #[serde(rename = "ip-address")] + pub ip_address: Option>, + #[serde(rename = "payment-methods")] + pub payment_methods: PaymentMethodContainer, + #[serde(rename = "parent-transaction-amount")] + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_transaction_amount: Option, + #[serde(rename = "api-id")] + pub api_id: String, + #[serde(rename = "self")] + pub self_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefundResponse { - id: String, - status: RefundStatus, + payment: RefundResponseData, } impl TryFrom> for RefundsRouterData { @@ -195,8 +808,8 @@ impl TryFrom> for RefundsRout ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.payment.transaction_id, + refund_status: enums::RefundStatus::from(item.response.payment.transaction_state), }), ..item.data }) @@ -210,15 +823,181 @@ impl TryFrom> for RefundsRouter ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.payment.transaction_id, + refund_status: enums::RefundStatus::from(item.response.payment.transaction_state), + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct CancelPaymentData { + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "parent-transaction-id")] + pub parent_transaction_id: String, + pub notifications: NotificationContainer, + #[serde(rename = "ip-address")] + pub ip_address: Option>, +} + +#[derive(Debug, Serialize)] +pub struct GetnetCancelRequest { + pub payment: CancelPaymentData, +} + +impl TryFrom<&PaymentsCancelRouterData> for GetnetCancelRequest { + type Error = error_stack::Report; + fn try_from(item: &PaymentsCancelRouterData) -> Result { + let request = &item.request; + let auth_type = GetnetAuthType::try_from(&item.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + + let merchant_account_id = MerchantAccountId { + value: auth_type.merchant_id, + }; + let webhook_url = &item.request.webhook_url; + let notifications = NotificationContainer { + format: NotificationFormat::JsonSigned, + + notification: vec![Notification { + url: webhook_url.clone(), + }], + }; + let capture_method = &item.request.capture_method; + let transaction_type = match capture_method { + Some(CaptureMethod::Automatic) => GetnetTransactionType::VoidPurchase, + Some(CaptureMethod::Manual) => GetnetTransactionType::VoidAuthorization, + Some(CaptureMethod::ManualMultiple) + | Some(CaptureMethod::Scheduled) + | Some(CaptureMethod::SequentialAutomatic) => { + return Err(errors::ConnectorError::CaptureMethodNotSupported {}.into()); + } + None => { + return Err(errors::ConnectorError::CaptureMethodNotSupported {}.into()); + } + }; + let ip_address = request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.ip_address.as_ref()) + .map(|ip| Secret::new(ip.to_string())); + let request_id = &item.connector_request_reference_id.clone(); + let parent_transaction_id = item.request.connector_transaction_id.clone(); + let cancel_payment_data = CancelPaymentData { + merchant_account_id, + request_id: request_id.to_string(), + transaction_type, + parent_transaction_id, + notifications, + ip_address, + }; + Ok(Self { + payment: cancel_payment_data, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum GetnetTransactionType { + Purchase, + #[serde(rename = "capture-authorization")] + CaptureAuthorization, + #[serde(rename = "refund-purchase")] + RefundPurchase, + #[serde(rename = "refund-capture")] + RefundCapture, + #[serde(rename = "void-authorization")] + VoidAuthorization, + #[serde(rename = "void-purchase")] + VoidPurchase, + Authorization, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub struct CancelResponseData { + pub statuses: Statuses, + pub descriptor: String, + pub notifications: NotificationContainer, + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "transaction-id")] + pub transaction_id: String, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "transaction-state")] + pub transaction_state: GetnetPaymentStatus, + #[serde(rename = "completion-time-stamp")] + pub completion_time_stamp: Option, + #[serde(rename = "requested-amount")] + pub requested_amount: Amount, + #[serde(rename = "parent-transaction-id")] + pub parent_transaction_id: String, + #[serde(rename = "account-holder")] + pub account_holder: Option, + #[serde(rename = "card-token")] + pub card_token: CardToken, + #[serde(rename = "ip-address")] + pub ip_address: Option>, + #[serde(rename = "payment-methods")] + pub payment_methods: PaymentMethodContainer, + #[serde(rename = "parent-transaction-amount")] + pub parent_transaction_amount: Amount, + #[serde(rename = "api-id")] + pub api_id: String, + #[serde(rename = "self")] + pub self_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetnetCancelResponse { + payment: CancelResponseData, +} + +pub fn cancel_status_from_transaction_state(getnet_status: GetnetPaymentStatus) -> AttemptStatus { + match getnet_status { + GetnetPaymentStatus::Success => AttemptStatus::Voided, + GetnetPaymentStatus::InProgress => AttemptStatus::Pending, + GetnetPaymentStatus::Failed => AttemptStatus::VoidFailed, + } +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + status: cancel_status_from_transaction_state(item.response.payment.transaction_state), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.payment.transaction_id, + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charges: None, }), ..item.data }) } } -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] pub struct GetnetErrorResponse { pub status_code: u16, @@ -226,3 +1005,150 @@ pub struct GetnetErrorResponse { pub message: String, pub reason: Option, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetnetWebhookNotificationResponse { + #[serde(rename = "response-signature-base64")] + pub response_signature_base64: Secret, + #[serde(rename = "response-signature-algorithm")] + pub response_signature_algorithm: Secret, + #[serde(rename = "response-base64")] + pub response_base64: Secret, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct WebhookResponseData { + pub statuses: Statuses, + pub descriptor: String, + pub notifications: NotificationContainer, + #[serde(rename = "merchant-account-id")] + pub merchant_account_id: MerchantAccountId, + #[serde(rename = "transaction-id")] + pub transaction_id: String, + #[serde(rename = "request-id")] + pub request_id: String, + #[serde(rename = "transaction-type")] + pub transaction_type: GetnetTransactionType, + #[serde(rename = "transaction-state")] + pub transaction_state: GetnetPaymentStatus, + #[serde(rename = "completion-time-stamp")] + pub completion_time_stamp: u64, + #[serde(rename = "requested-amount")] + pub requested_amount: Amount, + #[serde(rename = "parent-transaction-id")] + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_transaction_id: Option, + #[serde(rename = "account-holder")] + pub account_holder: Option, + #[serde(rename = "card-token")] + pub card_token: CardToken, + #[serde(rename = "ip-address")] + pub ip_address: Option>, + #[serde(rename = "payment-methods")] + pub payment_methods: PaymentMethodContainer, + #[serde(rename = "parent-transaction-amount")] + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_transaction_amount: Option, + #[serde(rename = "authorization-code")] + #[serde(skip_serializing_if = "Option::is_none")] + pub authorization_code: Option, + #[serde(rename = "api-id")] + pub api_id: String, + #[serde(rename = "provider-account-id")] + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_account_id: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetnetWebhookNotificationResponseBody { + pub payment: WebhookResponseData, +} + +pub fn is_refund_event(transaction_type: &GetnetTransactionType) -> bool { + matches!( + transaction_type, + GetnetTransactionType::RefundPurchase | GetnetTransactionType::RefundCapture + ) +} + +pub fn get_webhook_object_from_body( + body: &[u8], +) -> CustomResult { + let body_bytes = bytes::Bytes::copy_from_slice(body); + let parsed_param: GetnetWebhookNotificationResponse = + parse_url_encoded_to_struct(body_bytes) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let response_base64 = &parsed_param.response_base64.peek(); + let decoded_response = BASE64_ENGINE + .decode(response_base64) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let getnet_webhook_notification_response: GetnetWebhookNotificationResponseBody = + match serde_json::from_slice::(&decoded_response) { + Ok(response) => response, + Err(_e) => { + return Err(errors::ConnectorError::WebhookBodyDecodingFailed)?; + } + }; + + Ok(getnet_webhook_notification_response) +} + +pub fn get_webhook_response( + body: &[u8], +) -> CustomResult { + let body_bytes = bytes::Bytes::copy_from_slice(body); + let parsed_param: GetnetWebhookNotificationResponse = + parse_url_encoded_to_struct(body_bytes) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(parsed_param) +} + +pub fn get_incoming_webhook_event( + transaction_type: GetnetTransactionType, + transaction_status: GetnetPaymentStatus, +) -> IncomingWebhookEvent { + match transaction_type { + GetnetTransactionType::Purchase => match transaction_status { + GetnetPaymentStatus::Success => IncomingWebhookEvent::PaymentIntentSuccess, + GetnetPaymentStatus::Failed => IncomingWebhookEvent::PaymentIntentFailure, + GetnetPaymentStatus::InProgress => IncomingWebhookEvent::PaymentIntentProcessing, + }, + + GetnetTransactionType::Authorization => match transaction_status { + GetnetPaymentStatus::Success => IncomingWebhookEvent::PaymentIntentAuthorizationSuccess, + GetnetPaymentStatus::Failed => IncomingWebhookEvent::PaymentIntentAuthorizationFailure, + GetnetPaymentStatus::InProgress => IncomingWebhookEvent::PaymentIntentProcessing, + }, + + GetnetTransactionType::CaptureAuthorization => match transaction_status { + GetnetPaymentStatus::Success => IncomingWebhookEvent::PaymentIntentCaptureSuccess, + GetnetPaymentStatus::Failed => IncomingWebhookEvent::PaymentIntentCaptureFailure, + GetnetPaymentStatus::InProgress => IncomingWebhookEvent::PaymentIntentCaptureFailure, + }, + + GetnetTransactionType::RefundPurchase => match transaction_status { + GetnetPaymentStatus::Success => IncomingWebhookEvent::RefundSuccess, + GetnetPaymentStatus::Failed => IncomingWebhookEvent::RefundFailure, + GetnetPaymentStatus::InProgress => IncomingWebhookEvent::RefundFailure, + }, + + GetnetTransactionType::RefundCapture => match transaction_status { + GetnetPaymentStatus::Success => IncomingWebhookEvent::RefundSuccess, + GetnetPaymentStatus::Failed => IncomingWebhookEvent::RefundFailure, + GetnetPaymentStatus::InProgress => IncomingWebhookEvent::RefundFailure, + }, + + GetnetTransactionType::VoidAuthorization => match transaction_status { + GetnetPaymentStatus::Success => IncomingWebhookEvent::PaymentIntentCancelled, + GetnetPaymentStatus::Failed => IncomingWebhookEvent::PaymentIntentCancelFailure, + GetnetPaymentStatus::InProgress => IncomingWebhookEvent::PaymentIntentCancelFailure, + }, + + GetnetTransactionType::VoidPurchase => match transaction_status { + GetnetPaymentStatus::Success => IncomingWebhookEvent::PaymentIntentCancelled, + GetnetPaymentStatus::Failed => IncomingWebhookEvent::PaymentIntentCancelFailure, + GetnetPaymentStatus::InProgress => IncomingWebhookEvent::PaymentIntentCancelFailure, + }, + } +} diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index b082962e40..a13f98ed78 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1855,6 +1855,7 @@ pub trait PaymentsCaptureRequestData { fn get_optional_language_from_browser_info(&self) -> Option; fn is_multiple_capture(&self) -> bool; fn get_browser_info(&self) -> Result; + fn get_webhook_url(&self) -> Result; } impl PaymentsCaptureRequestData for PaymentsCaptureData { @@ -1871,6 +1872,11 @@ impl PaymentsCaptureRequestData for PaymentsCaptureData { .clone() .and_then(|browser_info| browser_info.language) } + fn get_webhook_url(&self) -> Result { + self.webhook_url + .clone() + .ok_or_else(missing_field_err("webhook_url")) + } } pub trait PaymentsSyncRequestData { @@ -1924,6 +1930,7 @@ pub trait PaymentsCancelRequestData { fn get_currency(&self) -> Result; fn get_cancellation_reason(&self) -> Result; fn get_browser_info(&self) -> Result; + fn get_webhook_url(&self) -> Result; } impl PaymentsCancelRequestData for PaymentsCancelData { @@ -1948,6 +1955,11 @@ impl PaymentsCancelRequestData for PaymentsCancelData { .clone() .and_then(|browser_info| browser_info.language) } + fn get_webhook_url(&self) -> Result { + self.webhook_url + .clone() + .ok_or_else(missing_field_err("webhook_url")) + } } pub trait RefundsRequestData { diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 6c1e11ced4..ff5534dceb 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -125,6 +125,7 @@ pub struct PaymentsCaptureData { pub minor_payment_amount: MinorUnit, pub minor_amount_to_capture: MinorUnit, pub integrity_object: Option, + pub webhook_url: Option, } #[derive(Debug, Clone, PartialEq)] @@ -467,6 +468,8 @@ pub struct PaymentsCancelData { // minor amount data for amount framework pub minor_amount: Option, + pub webhook_url: Option, + pub capture_method: Option, } #[derive(Debug, Default, Clone)] @@ -643,6 +646,7 @@ pub struct RefundsData { pub refund_status: storage_enums::RefundStatus, pub merchant_account_id: Option>, pub merchant_config_currency: Option, + pub capture_method: Option, } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/hyperswitch_interfaces/src/api.rs b/crates/hyperswitch_interfaces/src/api.rs index b70d326feb..cf9b2ff976 100644 --- a/crates/hyperswitch_interfaces/src/api.rs +++ b/crates/hyperswitch_interfaces/src/api.rs @@ -100,6 +100,11 @@ pub trait ConnectorIntegration: mime::APPLICATION_JSON.essence_str() } + /// fn get_content_type + fn get_accept_type(&self) -> &'static str { + mime::APPLICATION_JSON.essence_str() + } + /// primarily used when creating signature based on request method of payment flow fn get_http_method(&self) -> Method { Method::Post diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index ac14cfb7c1..caa24ae110 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -5332,6 +5332,62 @@ impl Default for settings::RequiredFields { common:HashMap::new(), } ), + ( + enums::Connector::Getnet, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "payment_method_data.card.card_network".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_network".to_string(), + display_name: "card_network".to_string(), + field_type: enums::FieldType::UserCardNetwork, + value: None, + } + ), + ] + ), + } + ), ( enums::Connector::Globalpay, RequiredFieldFinal { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 0da8bbf182..c79154bf68 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1382,10 +1382,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { forte::transformers::ForteAuthType::try_from(self.auth_type)?; Ok(()) } - // api_enums::Connector::Getnet => { - // getnet::transformers::GetnetAuthType::try_from(self.auth_type)?; - // Ok(()) - // } + api_enums::Connector::Getnet => { + getnet::transformers::GetnetAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Globalpay => { globalpay::transformers::GlobalpayAuthType::try_from(self.auth_type)?; Ok(()) diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index b5104391ab..308a3f2ede 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -627,6 +627,7 @@ impl minor_amount_to_capture: item.request.minor_amount, integrity_object: None, split_payments: item.request.split_payments, + webhook_url: item.request.webhook_url, }) } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 6e8152716d..54dca9b2e4 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -319,6 +319,7 @@ impl minor_amount_to_capture: item.request.minor_amount, integrity_object: None, split_payments: None, + webhook_url: None, }) } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index a34c137dbd..d528a320e6 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -473,6 +473,7 @@ pub async fn construct_payment_router_data_for_capture<'a>( metadata: payment_data.payment_intent.metadata.expose_option(), integrity_object: None, split_payments: None, + webhook_url: None, }; // TODO: evaluate the fields in router data, if they are required or not @@ -3436,6 +3437,7 @@ impl TryFrom> for types::PaymentsCaptureD metadata: payment_data.payment_intent.metadata.expose_option(), integrity_object: None, split_payments: None, + webhook_url: None, }) } } @@ -3466,6 +3468,21 @@ impl TryFrom> for types::PaymentsCaptureD field_name: "browser_info", })?; let amount = payment_data.payment_attempt.get_total_amount(); + + let router_base_url = &additional_data.router_base_url; + let attempt = &payment_data.payment_attempt; + + let merchant_connector_account_id = payment_data + .payment_attempt + .merchant_connector_id + .as_ref() + .map(|mca_id| mca_id.get_string_repr()) + .ok_or(errors::ApiErrorResponse::MerchantAccountNotFound)?; + let webhook_url: Option<_> = Some(helpers::create_webhook_url( + router_base_url, + &attempt.merchant_id, + merchant_connector_account_id, + )); Ok(Self { capture_method: payment_data.get_capture_method(), amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module @@ -3492,6 +3509,7 @@ impl TryFrom> for types::PaymentsCaptureD metadata: payment_data.payment_intent.metadata, integrity_object: None, split_payments: payment_data.payment_intent.split_payments, + webhook_url, }) } } @@ -3527,6 +3545,22 @@ impl TryFrom> for types::PaymentsCancelDa field_name: "browser_info", })?; let amount = payment_data.payment_attempt.get_total_amount(); + + let router_base_url = &additional_data.router_base_url; + let attempt = &payment_data.payment_attempt; + + let merchant_connector_account_id = payment_data + .payment_attempt + .merchant_connector_id + .as_ref() + .map(|mca_id| mca_id.get_string_repr()) + .ok_or(errors::ApiErrorResponse::MerchantAccountNotFound)?; + let webhook_url: Option<_> = Some(helpers::create_webhook_url( + router_base_url, + &attempt.merchant_id, + merchant_connector_account_id, + )); + let capture_method = payment_data.payment_attempt.capture_method; Ok(Self { amount: Some(amount.get_amount_as_i64()), // This should be removed once we start moving to connector module minor_amount: Some(amount), @@ -3539,6 +3573,8 @@ impl TryFrom> for types::PaymentsCancelDa connector_meta: payment_data.payment_attempt.connector_metadata, browser_info, metadata: payment_data.payment_intent.metadata, + webhook_url, + capture_method, }) } } diff --git a/crates/router/src/core/relay/utils.rs b/crates/router/src/core/relay/utils.rs index 969566c098..b4d70038c6 100644 --- a/crates/router/src/core/relay/utils.rs +++ b/crates/router/src/core/relay/utils.rs @@ -109,6 +109,7 @@ pub async fn construct_relay_refund_router_data( refund_status: common_enums::RefundStatus::from(relay_record.status), merchant_account_id: None, merchant_config_currency: None, + capture_method: None, }, response: Err(ErrorResponse::default()), diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 537f67871e..d9467d19ce 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -337,6 +337,7 @@ pub async fn construct_refund_router_data<'a, F>( })?; let connector_refund_id = refund.get_optional_connector_refund_id().cloned(); + let capture_method = payment_attempt.capture_method; let braintree_metadata = payment_intent .connector_metadata @@ -395,6 +396,7 @@ pub async fn construct_refund_router_data<'a, F>( refund_status: refund.refund_status, merchant_account_id, merchant_config_currency, + capture_method, }, response: Ok(types::RefundsResponseData { diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 9499eed165..8132d72026 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -442,9 +442,9 @@ impl ConnectorData { enums::Connector::Forte => { Ok(ConnectorEnum::Old(Box::new(connector::Forte::new()))) } - // enums::Connector::Getnet => { - // Ok(ConnectorEnum::Old(Box::new(connector::Getnet::new()))) - // } + enums::Connector::Getnet => { + Ok(ConnectorEnum::Old(Box::new(connector::Getnet::new()))) + } enums::Connector::Globalpay => { Ok(ConnectorEnum::Old(Box::new(connector::Globalpay::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2fa29ccaf9..450cf7c3d6 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -246,7 +246,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Fiservemea => Self::Fiservemea, api_enums::Connector::Fiuu => Self::Fiuu, api_enums::Connector::Forte => Self::Forte, - // api_enums::Connector::Getnet => Self::Getnet, + api_enums::Connector::Getnet => Self::Getnet, api_enums::Connector::Globalpay => Self::Globalpay, api_enums::Connector::Globepay => Self::Globepay, api_enums::Connector::Gocardless => Self::Gocardless, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index ead0173ed5..c874722551 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -408,6 +408,7 @@ pub trait ConnectorActions: Connector { refund_status: enums::RefundStatus::Pending, merchant_account_id: None, merchant_config_currency: None, + capture_method: None, }), payment_info, ); @@ -1075,6 +1076,7 @@ impl Default for PaymentRefundType { refund_status: enums::RefundStatus::Pending, merchant_account_id: None, merchant_config_currency: None, + capture_method: None, }; Self(data) } diff --git a/cypress-tests/cypress/e2e/configs/Payment/Getnet.js b/cypress-tests/cypress/e2e/configs/Payment/Getnet.js new file mode 100644 index 0000000000..ac80e20784 --- /dev/null +++ b/cypress-tests/cypress/e2e/configs/Payment/Getnet.js @@ -0,0 +1,330 @@ +const successfulNo3DSCardDetails = { + card_number: "5413330300001006", + card_exp_month: "02", + card_exp_year: "2027", + card_holder_name: "John Doe", + card_cvc: "006", + card_network: "Visa", +}; + +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + currency: "GBP", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + PaymentIntentWithShippingCost: { + Request: { + currency: "GBP", + shipping_cost: 50, + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + amount: 6000, + shipping_cost: 50, + }, + }, + }, + PaymentConfirmWithShippingCost: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + shipping_cost: 50, + amount_received: 6050, + amount: 6000, + net_amount: 6050, + }, + }, + }, + "3DSManualCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "GBP", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: "3DS payments is not supported by Getnet", + }, + }, + }, + }, + "3DSAutoCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "GBP", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: "3DS payments is not supported by Getnet", + }, + }, + }, + }, + No3DSManualCapture: { + Request: { + currency: "GBP", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + No3DSAutoCapture: { + Request: { + currency: "GBP", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + Capture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + amount_to_capture: 6000, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6000, + amount_capturable: 0, + amount_received: 6000, + }, + }, + }, + PartialCapture: { + Request: { + amount_to_capture: 2000, + }, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6000, + amount_capturable: 0, + amount_received: 2000, + }, + }, + }, + Refund: { + Request: { + amount: 6000, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + manualPaymentRefund: { + Request: { + amount: 6000, + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + manualPaymentPartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + amount: 2000, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + amount: 2000, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SyncRefund: { + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + ZeroAuthMandate: { + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Setup Mandate flow for Getnet is not implemented", + code: "IR_00", + }, + }, + }, + }, + ZeroAuthPaymentIntent: { + Request: { + amount: 0, + setup_future_usage: "off_session", + currency: "USD", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", + }, + }, + }, + ZeroAuthConfirmPayment: { + Request: { + payment_type: "setup_mandate", + payment_method: "card", + payment_method_type: "credit", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Setup Mandate flow for Getnet is not implemented", + code: "IR_00", + }, + }, + }, + }, + SaveCardUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + amount: 5000, + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSManualCapture: { + Request: { + payment_method: "card", + amount: 5000, + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/configs/Payment/Utils.js b/cypress-tests/cypress/e2e/configs/Payment/Utils.js index 4f383d6e2c..4c3f9a0852 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Utils.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Utils.js @@ -11,6 +11,7 @@ import { connectorDetails as datatransConnectorDetails } from "./Datatrans.js"; import { connectorDetails as elavonConnectorDetails } from "./Elavon.js"; import { connectorDetails as fiservemeaConnectorDetails } from "./Fiservemea.js"; import { connectorDetails as fiuuConnectorDetails } from "./Fiuu.js"; +import { connectorDetails as getnetConnectorDetails } from "./Getnet.js"; import { connectorDetails as iatapayConnectorDetails } from "./Iatapay.js"; import { connectorDetails as itaubankConnectorDetails } from "./ItauBank.js"; import { connectorDetails as jpmorganConnectorDetails } from "./Jpmorgan.js"; @@ -38,6 +39,7 @@ const connectorDetails = { cybersource: cybersourceConnectorDetails, deutschebank: deutschebankConnectorDetails, fiservemea: fiservemeaConnectorDetails, + getnet: getnetConnectorDetails, iatapay: iatapayConnectorDetails, itaubank: itaubankConnectorDetails, jpmorgan: jpmorganConnectorDetails,