feat(connector): implement auth and post auth flows for gpayments (#4746)

Co-authored-by: hrithikesh026 <hrithikesh.vm@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com>
This commit is contained in:
Kiran Kumar
2024-06-12 00:05:31 +05:30
committed by GitHub
parent 9f2476b99a
commit d93f65fd95
3 changed files with 459 additions and 23 deletions

View File

@ -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<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
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<RequestContent, errors::ConnectorError> {
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<Option<services::Request>, 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<ErrorResponse, errors::ConnectorError> {
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<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
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<Option<services::Request>, 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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl

View File

@ -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<String>,
}
#[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<String>,
pub error_message_type: Option<String>,
/// Always returns 'Error' to indicate that this message is an error.
///
/// Example: "Error"
pub message_type: String,
pub message_version: Option<String>,
#[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<String>,
pub browser_color_depth: Option<String>,
#[serde(rename = "browserIP")]
pub browser_ip: Option<Secret<String, common_utils::pii::IpAddress>>,
pub browser_javascript_enabled: Option<bool>,
pub browser_java_enabled: Option<bool>,
pub browser_language: Option<String>,
pub browser_screen_height: Option<String>,
pub browser_screen_width: Option<String>,
#[serde(rename = "browserTZ")]
pub browser_tz: Option<String>,
pub browser_user_agent: Option<String>,
}
#[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<url::Url>,
#[serde(rename = "acsReferenceNumber")]
pub acs_reference_number: String,
pub authentication_value: Option<String>,
}
#[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<AuthStatus> 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<String>,
pub trans_status: AuthStatus,
pub eci: Option<String>,
}

View File

@ -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<T> {
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<T> TryFrom<(i64, T)> for GpaymentsRouterData<T> {
}
}
//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<String>,
}
#[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<errors::ConnectorError>;
fn try_from(
item: &GpaymentsRouterData<&types::authentication::ConnectorAuthenticationRouterData>,
) -> Result<Self, Self::Error> {
let request = &item.router_data.request;
let browser_details = match request.browser_details.clone() {
Some(details) => Ok::<Option<types::BrowserInformation>, 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::<date_time::YYYYMMDDHHmmss>::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<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
api::Authentication,
GpaymentsAuthenticationSuccessResponse,
types::authentication::ConnectorAuthenticationRequestData,
types::authentication::AuthenticationResponseData,
>,
) -> Result<Self, Self::Error> {
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()?;