diff --git a/crates/router/src/connector/gpayments.rs b/crates/router/src/connector/gpayments.rs index 4d72788a2d2a..e628779cd97d 100644 --- a/crates/router/src/connector/gpayments.rs +++ b/crates/router/src/connector/gpayments.rs @@ -9,6 +9,7 @@ use transformers as gpayments; use crate::{ configs::settings, + connector::{gpayments::gpayments_types::GpaymentsConnectorMetaData, utils::to_connector_meta}, core::errors::{self, CustomResult}, events::connector_api_logs::ConnectorEvent, headers, services, @@ -214,8 +215,103 @@ impl types::authentication::AuthenticationResponseData, > for Gpayments { -} + fn get_headers( + &self, + req: &types::authentication::ConnectorAuthenticationRouterData, + connectors: &settings::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: &types::authentication::ConnectorAuthenticationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_metadata: GpaymentsConnectorMetaData = to_connector_meta( + req.request + .pre_authentication_data + .connector_metadata + .clone(), + )?; + Ok(connector_metadata.authentication_url) + } + + fn get_request_body( + &self, + req: &types::authentication::ConnectorAuthenticationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = gpayments::GpaymentsRouterData::try_from((0, req))?; + let req_obj = + gpayments_types::GpaymentsAuthenticationRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + fn build_request( + &self, + req: &types::authentication::ConnectorAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let gpayments_auth_type = gpayments::GpaymentsAuthType::try_from(&req.connector_auth_type)?; + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url( + &types::authentication::ConnectorAuthenticationType::get_url( + self, req, connectors, + )?, + ) + .attach_default_headers() + .headers( + types::authentication::ConnectorAuthenticationType::get_headers( + self, req, connectors, + )?, + ) + .set_body( + types::authentication::ConnectorAuthenticationType::get_request_body( + self, req, connectors, + )?, + ) + .add_certificate(Some(gpayments_auth_type.certificate)) + .add_certificate_key(Some(gpayments_auth_type.private_key)) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::authentication::ConnectorAuthenticationRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult< + types::authentication::ConnectorAuthenticationRouterData, + errors::ConnectorError, + > { + let response: gpayments_types::GpaymentsAuthenticationSuccessResponse = res + .response + .parse_struct("gpayments GpaymentsAuthenticationResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} impl ConnectorIntegration< api::PostAuthentication, @@ -223,6 +319,92 @@ impl types::authentication::AuthenticationResponseData, > for Gpayments { + fn get_headers( + &self, + req: &types::authentication::ConnectorPostAuthenticationRouterData, + connectors: &settings::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: &types::authentication::ConnectorPostAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let base_url = build_endpoint(self.base_url(connectors), &req.connector_meta_data)?; + Ok(format!( + "{}/api/v2/auth/brw/result?threeDSServerTransID={}", + base_url, req.request.threeds_server_transaction_id, + )) + } + + fn build_request( + &self, + req: &types::authentication::ConnectorPostAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let gpayments_auth_type = gpayments::GpaymentsAuthType::try_from(&req.connector_auth_type)?; + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url( + &types::authentication::ConnectorPostAuthenticationType::get_url( + self, req, connectors, + )?, + ) + .attach_default_headers() + .headers( + types::authentication::ConnectorPostAuthenticationType::get_headers( + self, req, connectors, + )?, + ) + .add_certificate(Some(gpayments_auth_type.certificate)) + .add_certificate_key(Some(gpayments_auth_type.private_key)) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::authentication::ConnectorPostAuthenticationRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult< + types::authentication::ConnectorPostAuthenticationRouterData, + errors::ConnectorError, + > { + let response: gpayments_types::GpaymentsPostAuthenticationResponse = res + .response + .parse_struct("gpayments PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + Ok( + types::authentication::ConnectorPostAuthenticationRouterData { + response: Ok( + types::authentication::AuthenticationResponseData::PostAuthNResponse { + trans_status: response.trans_status.into(), + authentication_value: response.authentication_value, + eci: response.eci, + }, + ), + ..data.clone() + }, + ) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } impl diff --git a/crates/router/src/connector/gpayments/gpayments_types.rs b/crates/router/src/connector/gpayments/gpayments_types.rs index c95e778bcbe5..cb436fe8830f 100644 --- a/crates/router/src/connector/gpayments/gpayments_types.rs +++ b/crates/router/src/connector/gpayments/gpayments_types.rs @@ -1,13 +1,13 @@ +use api_models::payments::ThreeDsCompletionIndicator; use cards::CardNumber; use common_utils::types; -use masking::{Deserialize, Serialize}; +use masking::{Deserialize, Secret, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct GpaymentsConnectorMetaData { pub authentication_url: String, pub three_ds_requestor_trans_id: Option, } - #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct GpaymentsPreAuthVersionCallRequest { @@ -38,6 +38,9 @@ pub struct TDS2ApiError { pub error_description: String, pub error_detail: Option, pub error_message_type: Option, + /// Always returns 'Error' to indicate that this message is an error. + /// + /// Example: "Error" pub message_type: String, pub message_version: Option, #[serde(rename = "sdkTransID")] @@ -122,3 +125,112 @@ pub struct GpaymentsPreAuthenticationResponse { #[serde(rename = "threeDSServerTransID")] pub three_ds_server_trans_id: String, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GpaymentsAuthenticationRequest { + pub acct_number: CardNumber, + pub authentication_ind: String, + pub browser_info_collected: BrowserInfoCollected, + pub card_expiry_date: String, + #[serde(rename = "notificationURL")] + pub notification_url: String, + pub merchant_id: String, + #[serde(rename = "threeDSCompInd")] + pub three_ds_comp_ind: ThreeDsCompletionIndicator, + pub message_category: String, + pub purchase_amount: String, + pub purchase_date: String, + #[serde(rename = "threeDSServerTransID")] + pub three_ds_server_trans_id: String, +} +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BrowserInfoCollected { + pub browser_accept_header: Option, + pub browser_color_depth: Option, + #[serde(rename = "browserIP")] + pub browser_ip: Option>, + pub browser_javascript_enabled: Option, + pub browser_java_enabled: Option, + pub browser_language: Option, + pub browser_screen_height: Option, + pub browser_screen_width: Option, + #[serde(rename = "browserTZ")] + pub browser_tz: Option, + pub browser_user_agent: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum AuthenticationInd { + #[serde(rename = "01")] + PaymentTransaction, + #[serde(rename = "02")] + RecurringTransaction, + #[serde(rename = "03")] + InstalmentTransaction, + #[serde(rename = "04")] + AddCard, + #[serde(rename = "05")] + MaintainCard, + #[serde(rename = "06")] + CardholderVerification, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GpaymentsAuthenticationSuccessResponse { + #[serde(rename = "dsReferenceNumber")] + pub ds_reference_number: String, + #[serde(rename = "dsTransID")] + pub ds_trans_id: String, + #[serde(rename = "threeDSServerTransID")] + pub three_ds_server_trans_id: String, + #[serde(rename = "messageVersion")] + pub message_version: String, + #[serde(rename = "transStatus")] + pub trans_status: AuthStatus, + #[serde(rename = "acsTransID")] + pub acs_trans_id: String, + #[serde(rename = "challengeUrl")] + pub acs_url: Option, + #[serde(rename = "acsReferenceNumber")] + pub acs_reference_number: String, + pub authentication_value: Option, +} + +#[derive(Deserialize, Debug, Clone, Serialize, PartialEq)] +pub enum AuthStatus { + /// Authentication/ Account Verification Successful + Y, + /// Not Authenticated /Account Not Verified; Transaction denied + N, + /// Authentication/ Account Verification Could Not Be Performed; Technical or other problem, as indicated in ARes or RReq + U, + /// Attempts Processing Performed; Not Authenticated/Verified , but a proof of attempted authentication/verification is provided + A, + /// Authentication/ Account Verification Rejected; Issuer is rejecting authentication/verification and request that authorisation not be attempted. + R, + /// Challenge required + C, +} + +impl From for common_enums::TransactionStatus { + fn from(value: AuthStatus) -> Self { + match value { + AuthStatus::Y => Self::Success, + AuthStatus::N => Self::Failure, + AuthStatus::U => Self::VerificationNotPerformed, + AuthStatus::A => Self::NotVerified, + AuthStatus::R => Self::Rejected, + AuthStatus::C => Self::ChallengeRequired, + } + } +} +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GpaymentsPostAuthenticationResponse { + pub authentication_value: Option, + pub trans_status: AuthStatus, + pub eci: Option, +} diff --git a/crates/router/src/connector/gpayments/transformers.rs b/crates/router/src/connector/gpayments/transformers.rs index ee98d26e388f..c6bce775e8f1 100644 --- a/crates/router/src/connector/gpayments/transformers.rs +++ b/crates/router/src/connector/gpayments/transformers.rs @@ -1,16 +1,25 @@ +use api_models::payments::DeviceChannel; +use base64::Engine; +use common_utils::date_time; use error_stack::ResultExt; -use masking::Secret; -use serde::{Deserialize, Serialize}; +use masking::{ExposeInterface, Secret}; +use serde::Deserialize; +use serde_json::to_string; use super::gpayments_types; use crate::{ - connector::utils, - consts, + connector::{ + gpayments::gpayments_types::{ + AuthStatus, BrowserInfoCollected, GpaymentsAuthenticationSuccessResponse, + }, + utils, + utils::{get_card_details, CardData}, + }, + consts::BASE64_ENGINE, core::errors, - types::{self, api}, + types::{self, api, api::MessageCategory, authentication::ChallengeParams}, }; -//TODO: Fill the struct with respective fields pub struct GpaymentsRouterData { pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. pub router_data: T, @@ -26,7 +35,6 @@ impl TryFrom<(i64, T)> for GpaymentsRouterData { } } -//TODO: Fill the struct with respective fields // Auth Struct pub struct GpaymentsAuthType { /// base64 encoded certificate @@ -68,15 +76,6 @@ impl TryFrom<&GpaymentsRouterData<&types::authentication::PreAuthNVersionCallRou } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct GpaymentsErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, -} - #[derive(Deserialize, PartialEq)] pub struct GpaymentsMetaData { pub endpoint_prefix: String, @@ -146,11 +145,9 @@ impl TryFrom<&GpaymentsRouterData<&types::authentication::PreAuthNRouterData>> acct_number: router_data.request.card_holder_account_number.clone(), card_scheme: None, challenge_window_size: Some(gpayments_types::ChallengeWindowSize::FullScreen), - // This is a required field but we don't listen to event callbacks event_callback_url: "https://webhook.site/55e3db24-7c4e-4432-9941-d806f68d210b" .to_string(), merchant_id: metadata.merchant_id, - // Since this feature is not in our favour, hard coded it to true skip_auto_browser_info_collect: Some(true), // should auto generate this id. three_ds_requestor_trans_id: uuid::Uuid::new_v4().hyphenated().to_string(), @@ -158,6 +155,151 @@ impl TryFrom<&GpaymentsRouterData<&types::authentication::PreAuthNRouterData>> } } +impl TryFrom<&GpaymentsRouterData<&types::authentication::ConnectorAuthenticationRouterData>> + for gpayments_types::GpaymentsAuthenticationRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: &GpaymentsRouterData<&types::authentication::ConnectorAuthenticationRouterData>, + ) -> Result { + let request = &item.router_data.request; + let browser_details = match request.browser_details.clone() { + Some(details) => Ok::, Self::Error>(Some(details)), + None => { + if request.device_channel == DeviceChannel::Browser { + Err(errors::ConnectorError::MissingRequiredField { + field_name: "browser_info", + })? + } else { + Ok(None) + } + } + }?; + let card_details = get_card_details(request.payment_method_data.clone(), "gpayments")?; + + let metadata = GpaymentsMetaData::try_from(&item.router_data.connector_meta_data)?; + + Ok(Self { + acct_number: card_details.card_number.clone(), + authentication_ind: "01".into(), + card_expiry_date: card_details.get_expiry_date_as_yymm()?.expose(), + merchant_id: metadata.merchant_id, + message_category: match item.router_data.request.message_category.clone() { + MessageCategory::Payment => "01".into(), + MessageCategory::NonPayment => "02".into(), + }, + notification_url: request + .return_url + .clone() + .ok_or(errors::ConnectorError::RequestEncodingFailed) + .attach_printable("missing return_url")?, + three_ds_comp_ind: request.threeds_method_comp_ind.clone(), + purchase_amount: item.amount.to_string(), + purchase_date: date_time::DateTime::::from(date_time::now()) + .to_string(), + three_ds_server_trans_id: request + .pre_authentication_data + .threeds_server_transaction_id + .clone(), + browser_info_collected: BrowserInfoCollected { + browser_javascript_enabled: browser_details + .as_ref() + .and_then(|details| details.java_script_enabled), + browser_accept_header: browser_details + .as_ref() + .and_then(|details| details.accept_header.clone()), + browser_ip: browser_details + .clone() + .and_then(|details| details.ip_address.map(|ip| Secret::new(ip.to_string()))), + browser_java_enabled: browser_details + .as_ref() + .and_then(|details| details.java_enabled), + browser_language: browser_details + .as_ref() + .and_then(|details| details.language.clone()), + browser_color_depth: browser_details + .as_ref() + .and_then(|details| details.color_depth.map(|a| a.to_string())), + browser_screen_height: browser_details + .as_ref() + .and_then(|details| details.screen_height.map(|a| a.to_string())), + browser_screen_width: browser_details + .as_ref() + .and_then(|details| details.screen_width.map(|a| a.to_string())), + browser_tz: browser_details + .as_ref() + .and_then(|details| details.time_zone.map(|a| a.to_string())), + browser_user_agent: browser_details + .as_ref() + .and_then(|details| details.user_agent.clone().map(|a| a.to_string())), + }, + }) + } +} +impl + TryFrom< + types::ResponseRouterData< + api::Authentication, + GpaymentsAuthenticationSuccessResponse, + types::authentication::ConnectorAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + >, + > for types::authentication::ConnectorAuthenticationRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + api::Authentication, + GpaymentsAuthenticationSuccessResponse, + types::authentication::ConnectorAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + >, + ) -> Result { + let response_auth = item.response; + let creq = serde_json::json!({ + "threeDSServerTransID": response_auth.three_ds_server_trans_id, + "acsTransID": response_auth.acs_trans_id, + "messageVersion": response_auth.message_version, + "messageType": "CReq", + "challengeWindowSize": "01", + }); + let creq_str = to_string(&creq) + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .attach_printable("error while constructing creq_str")?; + let creq_base64 = Engine::encode(&BASE64_ENGINE, creq_str) + .trim_end_matches('=') + .to_owned(); + let response: Result< + types::authentication::AuthenticationResponseData, + types::ErrorResponse, + > = Ok( + types::authentication::AuthenticationResponseData::AuthNResponse { + trans_status: response_auth.trans_status.clone().into(), + authn_flow_type: if response_auth.trans_status == AuthStatus::C { + types::authentication::AuthNFlowType::Challenge(Box::new(ChallengeParams { + acs_url: response_auth.acs_url, + challenge_request: Some(creq_base64), + acs_reference_number: Some(response_auth.acs_reference_number.clone()), + acs_trans_id: Some(response_auth.acs_trans_id.clone()), + three_dsserver_trans_id: Some(response_auth.three_ds_server_trans_id), + acs_signed_content: None, + })) + } else { + types::authentication::AuthNFlowType::Frictionless + }, + authentication_value: response_auth.authentication_value, + ds_trans_id: Some(response_auth.ds_trans_id), + connector_metadata: None, + }, + ); + Ok(Self { + response, + ..item.data.clone() + }) + } +} + impl TryFrom< types::ResponseRouterData< @@ -186,11 +328,11 @@ impl "threeDSServerTransID": threeds_method_response.three_ds_server_trans_id, "threeDSMethodNotificationURL": "https://webhook.site/bd06863d-82c2-42ea-b35b-5ffd5ecece71" }); - serde_json::to_string(&three_ds_method_data_json) + to_string(&three_ds_method_data_json) .change_context(errors::ConnectorError::ResponseDeserializationFailed) .attach_printable("error while constructing three_ds_method_data_str") .map(|three_ds_method_data_string| { - base64::Engine::encode(&consts::BASE64_ENGINE, three_ds_method_data_string) + Engine::encode(&BASE64_ENGINE, three_ds_method_data_string) }) }) .transpose()?;