diff --git a/config/Development.toml b/config/Development.toml index c5b1911913..06b4d544c0 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -85,6 +85,8 @@ base_url = "https://api.shift4.com/" [connectors.worldpay] base_url = "http://localhost:9090/" +[connectors.payu] +base_url = "https://secure.snd.payu.com/api/" [connectors.globalpay] base_url = "https://apis.sandbox.globalpay.com/ucp/" diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 79eee90f40..cdb2f70d1f 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -505,6 +505,7 @@ pub enum Connector { Dummy, Globalpay, Klarna, + Payu, Shift4, Stripe, Worldpay, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 474298ff07..0707dfb1db 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -122,17 +122,18 @@ pub struct SupportedConnectors { pub struct Connectors { pub aci: ConnectorParams, pub adyen: ConnectorParams, + pub applepay: ConnectorParams, pub authorizedotnet: ConnectorParams, pub braintree: ConnectorParams, pub checkout: ConnectorParams, - pub klarna: ConnectorParams, pub cybersource: ConnectorParams, + pub globalpay: ConnectorParams, + pub klarna: ConnectorParams, + pub payu: ConnectorParams, pub shift4: ConnectorParams, pub stripe: ConnectorParams, pub supported: SupportedConnectors, - pub globalpay: ConnectorParams, pub worldpay: ConnectorParams, - pub applepay: ConnectorParams, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index f6428ed6d9..a790601e5e 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -7,6 +7,7 @@ pub mod checkout; pub mod cybersource; pub mod globalpay; pub mod klarna; +pub mod payu; pub mod shift4; pub mod stripe; pub mod utils; @@ -15,5 +16,5 @@ pub mod worldpay; pub use self::{ aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet, braintree::Braintree, checkout::Checkout, cybersource::Cybersource, globalpay::Globalpay, - klarna::Klarna, shift4::Shift4, stripe::Stripe, worldpay::Worldpay, + klarna::Klarna, payu::Payu, shift4::Shift4, stripe::Stripe, worldpay::Worldpay, }; diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs new file mode 100644 index 0000000000..da46caa247 --- /dev/null +++ b/crates/router/src/connector/payu.rs @@ -0,0 +1,615 @@ +mod transformers; + +use std::fmt::Debug; + +use bytes::Bytes; +use error_stack::{IntoReport, ResultExt}; +use transformers as payu; + +use crate::{ + configs::settings, + core::{ + errors::{self, CustomResult}, + payments, + }, + headers, logger, services, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Payu; + +impl ConnectorCommonExt for Payu +where + Self: services::ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::PaymentsAuthorizeType::get_content_type(self).to_string(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Payu { + fn id(&self) -> &'static str { + "payu" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.payu.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth: payu::PayuAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)]) + } + fn build_error_response( + &self, + res: Bytes, + ) -> CustomResult { + let response: payu::PayuErrorResponse = res + .parse_struct("Payu ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + code: response.status.status_code, + message: response.status.status_desc, + reason: response.status.code_literal, + }) + } +} + +impl api::Payment for Payu {} + +impl api::PreVerify for Payu {} +impl + services::ConnectorIntegration< + api::Verify, + types::VerifyRequestData, + types::PaymentsResponseData, + > for Payu +{ +} + +impl api::PaymentVoid for Payu {} + +impl + services::ConnectorIntegration< + api::Void, + types::PaymentsCancelData, + types::PaymentsResponseData, + > for Payu +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + 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::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = &req.request.connector_transaction_id; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "v2_1/orders/", + connector_payment_id + )) + } + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Delete) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .build(); + Ok(Some(request)) + } + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: payu::PayuPaymentsCancelResponse = res + .response + .parse_struct("PaymentCancelResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(payments_create_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentSync for Payu {} +impl + services::ConnectorIntegration + for Payu +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + 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::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "v2_1/orders/", + connector_payment_id + )) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(target: "router::connector::payu", response=?res); + let response: payu::PayuPaymentsSyncResponse = res + .response + .parse_struct("payu OrderResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentCapture for Payu {} +impl + services::ConnectorIntegration< + api::Capture, + types::PaymentsCaptureData, + types::PaymentsResponseData, + > for Payu +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + 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::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}{}", + self.base_url(connectors), + "v2_1/orders/", + req.request.connector_transaction_id, + "/status" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + let payu_req = utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payu_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Put) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: payu::PayuPaymentsCaptureResponse = res + .response + .parse_struct("payu CaptureResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentSession for Payu {} + +impl + services::ConnectorIntegration< + api::Session, + types::PaymentsSessionData, + types::PaymentsResponseData, + > for Payu +{ + //TODO: implement sessions flow +} + +impl api::PaymentAuthorize for Payu {} + +impl + services::ConnectorIntegration< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + > for Payu +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + 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::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "v2_1/orders")) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let payu_req = utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payu_req)) + } + + fn build_request( + &self, + req: &types::RouterData< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: payu::PayuPaymentsResponse = res + .response + .parse_struct("PayuPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(payupayments_create_response=?response); + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::Refund for Payu {} +impl api::RefundExecute for Payu {} +impl api::RefundSync for Payu {} + +impl services::ConnectorIntegration + for Payu +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + 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::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}{}", + self.base_url(connectors), + "v2_1/orders/", + req.request.connector_transaction_id, + "/refund" + )) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let payu_req = utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payu_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + logger::debug!(target: "router::connector::payu", response=?res); + let response: payu::RefundResponse = res + .response + .parse_struct("payu RefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl services::ConnectorIntegration + for Payu +{ + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + 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::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}{}", + self.base_url(connectors), + "v2_1/orders/", + req.request.connector_transaction_id, + "/refunds" + )) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(target: "router::connector::payu", response=?res); + let response: payu::RefundSyncResponse = + res.response + .parse_struct("payu RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Bytes, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Payu { + fn get_webhook_object_reference_id( + &self, + _body: &[u8], + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _body: &[u8], + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _body: &[u8], + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} + +impl services::ConnectorRedirectResponse for Payu { + fn get_flow_type( + &self, + _query_params: &str, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/payu/transformers.rs b/crates/router/src/connector/payu/transformers.rs new file mode 100644 index 0000000000..1a526ef373 --- /dev/null +++ b/crates/router/src/connector/payu/transformers.rs @@ -0,0 +1,564 @@ +use base64::Engine; +use error_stack::{IntoReport, ResultExt}; +use serde::{Deserialize, Serialize}; + +use crate::{ + consts, + core::errors, + pii::{self, Secret}, + types::{self, api, storage::enums}, + utils::OptionExt, +}; + +const WALLET_IDENTIFIER: &str = "PBL"; + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuPaymentsRequest { + customer_ip: std::net::IpAddr, + merchant_pos_id: String, + total_amount: i64, + currency_code: enums::Currency, + description: String, + pay_methods: PayuPaymentMethod, + continue_url: Option, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PayuPaymentMethod { + pay_method: PayuPaymentMethodData, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(untagged)] +pub enum PayuPaymentMethodData { + Card(PayuCard), + Wallet(PayuWallet), +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum PayuCard { + #[serde(rename_all = "camelCase")] + Card { + number: Secret, + expiration_month: Secret, + expiration_year: Secret, + cvv: Secret, + }, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PayuWallet { + pub value: PayuWalletCode, + #[serde(rename = "type")] + pub wallet_type: String, + pub authorization_code: String, +} +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum PayuWalletCode { + Ap, + Jp, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayuPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let auth_type = PayuAuthType::try_from(&item.connector_auth_type)?; + let payment_method = match item.request.payment_method_data.clone() { + api::PaymentMethod::Card(ccard) => Ok(PayuPaymentMethod { + pay_method: PayuPaymentMethodData::Card(PayuCard::Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + cvv: ccard.card_cvc, + }), + }), + api::PaymentMethod::Wallet(wallet_data) => match wallet_data.issuer_name { + api_models::enums::WalletIssuer::GooglePay => Ok(PayuPaymentMethod { + pay_method: PayuPaymentMethodData::Wallet({ + PayuWallet { + value: PayuWalletCode::Ap, + wallet_type: WALLET_IDENTIFIER.to_string(), + authorization_code: consts::BASE64_ENGINE.encode( + wallet_data + .token + .get_required_value("token") + .change_context(errors::ConnectorError::RequestEncodingFailed) + .attach_printable("No token passed")?, + ), + } + }), + }), + api_models::enums::WalletIssuer::ApplePay => Ok(PayuPaymentMethod { + pay_method: PayuPaymentMethodData::Wallet({ + PayuWallet { + value: PayuWalletCode::Jp, + wallet_type: WALLET_IDENTIFIER.to_string(), + authorization_code: consts::BASE64_ENGINE.encode( + wallet_data + .token + .get_required_value("token") + .change_context(errors::ConnectorError::RequestEncodingFailed) + .attach_printable("No token passed")?, + ), + } + }), + }), + _ => Err(errors::ConnectorError::NotImplemented( + "Unknown Wallet in Payment Method".to_string(), + )), + }, + _ => Err(errors::ConnectorError::NotImplemented( + "Unknown payment method".to_string(), + )), + }?; + let browser_info = item.request.browser_info.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "browser_info".to_string(), + }, + )?; + Ok(Self { + customer_ip: browser_info.ip_address.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "browser_info.ip_address".to_string(), + }, + )?, + merchant_pos_id: auth_type.merchant_pos_id, + total_amount: item.request.amount, + currency_code: item.request.currency, + description: item.description.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "item.description".to_string(), + }, + )?, + pay_methods: payment_method, + continue_url: None, + }) + } +} + +pub struct PayuAuthType { + pub(super) api_key: String, + pub(super) merchant_pos_id: String, +} + +impl TryFrom<&types::ConnectorAuthType> for PayuAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + api_key: api_key.to_string(), + merchant_pos_id: key1.to_string(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayuPaymentStatus { + Success, + WarningContinueRedirect, + #[serde(rename = "WARNING_CONTINUE_3DS")] + WarningContinue3ds, + WarningContinueCvv, + #[default] + Pending, +} + +impl From for enums::AttemptStatus { + fn from(item: PayuPaymentStatus) -> Self { + match item { + PayuPaymentStatus::Success => Self::Pending, + PayuPaymentStatus::WarningContinue3ds => Self::Pending, + PayuPaymentStatus::WarningContinueCvv => Self::Pending, + PayuPaymentStatus::WarningContinueRedirect => Self::Pending, + PayuPaymentStatus::Pending => Self::Pending, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuPaymentsResponse { + pub status: PayuPaymentStatusData, + pub redirect_uri: String, + pub iframe_allowed: Option, + pub three_ds_protocol_version: Option, + pub order_id: String, + pub ext_order_id: Option, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status.status_code), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id), + redirect: false, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }), + amount_captured: None, + ..item.data + }) + } +} + +#[derive(Default, Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuPaymentsCaptureRequest { + order_id: String, + order_status: OrderStatus, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for PayuPaymentsCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + Ok(Self { + order_id: item.request.connector_transaction_id.clone(), + order_status: OrderStatus::Completed, + }) + } +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct PayuPaymentsCaptureResponse { + status: PayuPaymentStatusData, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PayuPaymentsCaptureResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status.status_code.clone()), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirect: false, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }), + amount_captured: None, + ..item.data + }) + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuPaymentsCancelResponse { + pub order_id: String, + pub ext_order_id: Option, + pub status: PayuPaymentStatusData, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PayuPaymentsCancelResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status.status_code.clone()), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id), + redirect: false, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }), + amount_captured: None, + ..item.data + }) + } +} + +#[allow(dead_code)] +#[derive(Debug, Serialize, Eq, PartialEq, Default, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderStatus { + New, + Canceled, + Completed, + WaitingForConfirmation, + #[default] + Pending, +} + +impl From for enums::AttemptStatus { + fn from(item: OrderStatus) -> Self { + match item { + OrderStatus::New => Self::PaymentMethodAwaited, + OrderStatus::Canceled => Self::Voided, + OrderStatus::Completed => Self::Charged, + OrderStatus::Pending => Self::Pending, + OrderStatus::WaitingForConfirmation => Self::Authorized, + } + } +} + +#[derive(Debug, Serialize, Default, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuPaymentStatusData { + status_code: PayuPaymentStatus, + severity: Option, + status_desc: Option, +} +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuProductData { + name: String, + unit_price: String, + quantity: String, + #[serde(rename = "virtual")] + virtually: Option, + listing_date: Option, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuOrderResponseData { + order_id: String, + ext_order_id: Option, + order_create_date: String, + notify_url: Option, + customer_ip: std::net::IpAddr, + merchant_pos_id: String, + description: String, + validity_time: Option, + currency_code: enums::Currency, + total_amount: String, + buyer: Option, + pay_method: Option, + products: Option>, + status: OrderStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuOrderResponseBuyerData { + ext_customer_id: Option, + email: Option, + phone: Option, + first_name: Option, + last_name: Option, + nin: Option, + language: Option, + delivery: Option, + customer_id: Option, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayuOrderResponsePayMethod { + CardToken, + Pbl, + Installemnts, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PayuOrderResponseProperty { + name: String, + value: String, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct PayuPaymentsSyncResponse { + orders: Vec, + status: PayuPaymentStatusData, + properties: Option>, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PayuPaymentsSyncResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let order = match item.response.orders.first() { + Some(order) => order, + _ => Err(errors::ConnectorError::ResponseHandlingFailed)?, + }; + Ok(Self { + status: enums::AttemptStatus::from(order.status.clone()), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(order.order_id.clone()), + redirect: false, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }), + amount_captured: Some( + order + .total_amount + .parse::() + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?, + ), + ..item.data + }) + } +} + +#[derive(Default, Debug, Eq, PartialEq, Serialize)] +pub struct PayuRefundRequestData { + description: String, + amount: Option, +} + +#[derive(Default, Debug, Serialize)] +pub struct PayuRefundRequest { + refund: PayuRefundRequestData, +} + +impl TryFrom<&types::RefundsRouterData> for PayuRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + Ok(Self { + refund: PayuRefundRequestData { + description: item.description.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "item.description".to_string(), + }, + )?, + amount: None, + }, + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Eq, PartialEq, Default, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub enum RefundStatus { + Finalized, + Canceled, + #[default] + Pending, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Finalized => Self::Success, + RefundStatus::Canceled => Self::Failure, + RefundStatus::Pending => Self::Pending, + } + } +} + +#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PayuRefundResponseData { + refund_id: String, + ext_refund_id: String, + amount: String, + currency_code: enums::Currency, + description: String, + creation_date_time: String, + status: RefundStatus, + status_date_time: Option, +} + +#[derive(Default, Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RefundResponse { + refund: PayuRefundResponseData, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund_status = enums::RefundStatus::from(item.response.refund.status); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.refund.refund_id, + refund_status, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Clone, Deserialize)] +pub struct RefundSyncResponse { + refunds: Vec, +} +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund = match item.response.refunds.first() { + Some(refund) => refund, + _ => Err(errors::ConnectorError::ResponseHandlingFailed)?, + }; + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: refund.refund_id.clone(), + refund_status: enums::RefundStatus::from(refund.status.clone()), + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PayuErrorData { + pub status_code: String, + pub code: Option, + pub code_literal: Option, + pub status_desc: String, +} +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct PayuErrorResponse { + pub status: PayuErrorData, +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 8cfc61670d..1cb9c7a4e5 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -233,6 +233,7 @@ async fn send_request( logger::debug!(?url_encoded_payload); client.body(url_encoded_payload).send() } + // If payload needs processing the body cannot have default None => client .body(request.payload.expose_option().unwrap_or_default()) .send(), @@ -240,7 +241,14 @@ async fn send_request( .await } - Method::Put => client.put(url).add_headers(headers).send().await, + Method::Put => { + client + .put(url) + .add_headers(headers) + .body(request.payload.expose_option().unwrap_or_default()) // If payload needs processing the body cannot have default + .send() + .await + } Method::Delete => client.delete(url).add_headers(headers).send().await, } .map_err(|error| match error { @@ -260,7 +268,7 @@ async fn handle_response( logger::info!(?response); let status_code = response.status().as_u16(); match status_code { - 200..=202 => { + 200..=202 | 302 => { logger::debug!(response=?response); // If needed add log line // logger:: error!( error_parsing_response=?err); diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index 6d2f01b852..e8b8409f02 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -41,7 +41,7 @@ pub(super) fn create_client( client_certificate: Option, client_certificate_key: Option, ) -> CustomResult { - let mut client_builder = reqwest::Client::builder(); + let mut client_builder = reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()); if !should_bypass_proxy { if let Some(url) = ProxyType::Http.get_proxy_url(proxy) { diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 87b32f24c5..f9feb7e944 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -150,6 +150,7 @@ impl ConnectorData { "cybersource" => Ok(Box::new(&connector::Cybersource)), "shift4" => Ok(Box::new(&connector::Shift4)), "worldpay" => Ok(Box::new(&connector::Worldpay)), + "payu" => Ok(Box::new(&connector::Payu)), "globalpay" => Ok(Box::new(&connector::Globalpay)), _ => Err(report!(errors::ConnectorError::InvalidConnectorName) .attach_printable(format!("invalid connector name: {connector_name}"))) diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 24916e6582..86683b0f7a 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -7,6 +7,7 @@ pub(crate) struct ConnectorAuthentication { pub authorizedotnet: Option, pub checkout: Option, pub globalpay: Option, + pub payu: Option, pub shift4: Option, pub worldpay: Option, } diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index c3f3517995..b0b44c89cb 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -5,6 +5,7 @@ mod authorizedotnet; mod checkout; mod connector_auth; mod globalpay; +mod payu; mod shift4; mod utils; mod worldpay; diff --git a/crates/router/tests/connectors/payu.rs b/crates/router/tests/connectors/payu.rs new file mode 100644 index 0000000000..ecfd0426e7 --- /dev/null +++ b/crates/router/tests/connectors/payu.rs @@ -0,0 +1,277 @@ +use router::types::{self, api, storage::enums}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions, PaymentAuthorizeType}, +}; + +struct Payu; +impl ConnectorActions for Payu {} +impl utils::Connector for Payu { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Payu; + types::api::ConnectorData { + connector: Box::new(&Payu), + connector_name: types::Connector::Payu, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .payu + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "payu".to_string() + } +} + +#[actix_web::test] +async fn should_authorize_card_payment() { + //Authorize Card Payment in PLN currenct + let authorize_response = Payu {} + .authorize_payment( + Some(types::PaymentsAuthorizeData { + currency: enums::Currency::PLN, + ..PaymentAuthorizeType::default().0 + }), + None, + ) + .await; + // in Payu need Psync to get status therfore set to pending + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + let sync_response = Payu {} + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + transaction_id.clone(), + ), + encoded_data: None, + }), + None, + ) + .await; + // Assert the sync response, it will be authorized in case of manual capture, for automatic it will be Completed Success + assert_eq!(sync_response.status, enums::AttemptStatus::Authorized); + } +} + +#[actix_web::test] +async fn should_authorize_gpay_payment() { + let authorize_response = Payu {}.authorize_payment(Some(types::PaymentsAuthorizeData{ + payment_method_data: types::api::PaymentMethod::Wallet(api::WalletData{ + issuer_name: api_models::enums::WalletIssuer::GooglePay, + token: Some(r#"{"signature":"MEUCIQD7Ta+d9+buesrH2KKkF+03AqTen+eHHN8KFleHoKaiVAIgGvAXyI0Vg3ws8KlF7agW/gmXJhpJOOPkqiNVbn/4f0Y\u003d","protocolVersion":"ECv1","signedMessage":"{\"encryptedMessage\":\"UcdGP9F/1loU0aXvVj6VqGRPA5EAjHYfJrXD0N+5O13RnaJXKWIjch1zzjpy9ONOZHqEGAqYKIcKcpe5ppN4Fpd0dtbm1H4u+lA+SotCff3euPV6sne22/Pl/MNgbz5QvDWR0UjcXvIKSPNwkds1Ib7QMmH4GfZ3vvn6s534hxAmcv/LlkeM4FFf6py9crJK5fDIxtxRJncfLuuPeAXkyy+u4zE33HmT34Oe5MSW/kYZVz31eWqFy2YCIjbJcC9ElMluoOKSZ305UG7tYGB1LCFGQLtLxphrhPu1lEmGEZE1t2cVDoCzjr3rm1OcfENc7eNC4S+ko6yrXh1ZX06c/F9kunyLn0dAz8K5JLIwLdjw3wPADVSd3L0eM7jkzhH80I6nWkutO0x8BFltxWl+OtzrnAe093OUncH6/DK1pCxtJaHdw1WUWrzULcdaMZmPfA\\u003d\\u003d\",\"ephemeralPublicKey\":\"BH7A1FUBWiePkjh/EYmsjY/63D/6wU+4UmkLh7WW6v7PnoqQkjrFpc4kEP5a1Op4FkIlM9LlEs3wGdFB8xIy9cM\\u003d\",\"tag\":\"e/EOsw2Y2wYpJngNWQqH7J62Fhg/tzmgDl6UFGuAN+A\\u003d\"}"}"# + .to_string()) //Generate new GooglePay token this is bound to expire + }), + currency: enums::Currency::PLN, + ..PaymentAuthorizeType::default().0 + }), None).await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + let sync_response = Payu {} + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + transaction_id.clone(), + ), + encoded_data: None, + }), + None, + ) + .await; + assert_eq!(sync_response.status, enums::AttemptStatus::Authorized); + } +} + +#[actix_web::test] +async fn should_capture_already_authorized_payment() { + let connector = Payu {}; + let authorize_response = connector + .authorize_payment( + Some(types::PaymentsAuthorizeData { + currency: enums::Currency::PLN, + ..PaymentAuthorizeType::default().0 + }), + None, + ) + .await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + let sync_response = connector + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + transaction_id.clone(), + ), + encoded_data: None, + }), + None, + ) + .await; + assert_eq!(sync_response.status, enums::AttemptStatus::Authorized); + let capture_response = connector + .capture_payment(transaction_id.clone(), None, None) + .await; + assert_eq!(capture_response.status, enums::AttemptStatus::Pending); + let response = connector + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + transaction_id, + ), + encoded_data: None, + }), + None, + ) + .await; + assert_eq!(response.status, enums::AttemptStatus::Charged,); + } +} + +#[actix_web::test] +async fn should_sync_payment() { + let connector = Payu {}; + // Authorize the payment for manual capture + let authorize_response = connector + .authorize_payment( + Some(types::PaymentsAuthorizeData { + currency: enums::Currency::PLN, + ..PaymentAuthorizeType::default().0 + }), + None, + ) + .await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + // Sync the Payment Data + let response = connector + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + transaction_id, + ), + encoded_data: None, + }), + None, + ) + .await; + + assert_eq!(response.status, enums::AttemptStatus::Authorized); + } +} + +#[actix_web::test] +async fn should_void_already_authorized_payment() { + let connector = Payu {}; + //make a successful payment + let authorize_response = connector + .make_payment( + Some(types::PaymentsAuthorizeData { + currency: enums::Currency::PLN, + ..PaymentAuthorizeType::default().0 + }), + None, + ) + .await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + + //try CANCEL for previous payment + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + let void_response = connector + .void_payment(transaction_id.clone(), None, None) + .await; + assert_eq!(void_response.status, enums::AttemptStatus::Pending); + + let sync_response = connector + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + transaction_id, + ), + encoded_data: None, + }), + None, + ) + .await; + assert_eq!(sync_response.status, enums::AttemptStatus::Voided,); + } +} + +#[actix_web::test] +async fn should_refund_succeeded_payment() { + let connector = Payu {}; + //make a successful payment + let authorize_response = connector + .make_payment( + Some(types::PaymentsAuthorizeData { + currency: enums::Currency::PLN, + ..PaymentAuthorizeType::default().0 + }), + None, + ) + .await; + assert_eq!(authorize_response.status, enums::AttemptStatus::Pending); + + if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) { + //Capture the payment in case of Manual Capture + let capture_response = connector + .capture_payment(transaction_id.clone(), None, None) + .await; + assert_eq!(capture_response.status, enums::AttemptStatus::Pending); + + let sync_response = connector + .sync_payment( + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + transaction_id.clone(), + ), + encoded_data: None, + }), + None, + ) + .await; + assert_eq!(sync_response.status, enums::AttemptStatus::Charged); + + //Refund the payment + let refund_response = connector + .refund_payment(transaction_id.clone(), None, None) + .await; + assert_eq!( + refund_response.response.unwrap().connector_refund_id.len(), + 10 + ); + } +} + +#[actix_web::test] +async fn should_sync_succeeded_refund_payment() { + let connector = Payu {}; + + //Currently hardcoding the order_id because RSync is not instant, change it accordingly + let sync_refund_response = connector + .sync_refund("6DHQQN3T57230110GUEST000P01".to_string(), None, None) + .await; + assert_eq!( + sync_refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success + ); +} + +#[actix_web::test] +async fn should_fail_already_refunded_payment() { + let connector = Payu {}; + //Currently hardcoding the order_id, change it accordingly + let response = connector + .refund_payment("5H1SVX6P7W230112GUEST000P01".to_string(), None, None) + .await; + let x = response.response.unwrap_err(); + assert_eq!(x.reason.unwrap(), "PAID".to_string()); +} diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 3dcf495e9a..a8fe5dffe2 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -19,5 +19,9 @@ api_key = "Bearer MyApiKey" [worldpay] api_key = "Bearer MyApiKey" +[payu] +api_key = "Bearer MyApiKey" +key1 = "MerchantPosId" + [globalpay] api_key = "Bearer MyApiKey" diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 70576c37c0..650eafd7bf 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -135,7 +135,7 @@ pub trait ConnectorActions: Connector { let integration = self.get_data().connector.get_connector_integration(); let request = self.generate_data( payment_data.unwrap_or_else(|| types::RefundsData { - amount: 100, + amount: 1000, currency: enums::Currency::USD, refund_id: uuid::Uuid::new_v4().to_string(), connector_transaction_id: transaction_id, @@ -230,6 +230,7 @@ pub struct PaymentAuthorizeType(pub types::PaymentsAuthorizeData); pub struct PaymentSyncType(pub types::PaymentsSyncData); pub struct PaymentRefundType(pub types::RefundsData); pub struct CCardType(pub api::CCard); +pub struct BrowserInfoType(pub types::BrowserInformation); impl Default for CCardType { fn default() -> Self { @@ -256,7 +257,7 @@ impl Default for PaymentAuthorizeType { mandate_id: None, off_session: None, setup_mandate_details: None, - browser_info: None, + browser_info: Some(BrowserInfoType::default().0), order_details: None, email: None, }; @@ -264,6 +265,24 @@ impl Default for PaymentAuthorizeType { } } +impl Default for BrowserInfoType { + fn default() -> Self { + let data = types::BrowserInformation { + user_agent: "".to_string(), + accept_header: "".to_string(), + language: "nl-NL".to_string(), + color_depth: 24, + screen_height: 723, + screen_width: 1536, + time_zone: 0, + java_enabled: true, + java_script_enabled: true, + ip_address: Some("127.0.0.1".parse().unwrap()), + }; + Self(data) + } +} + impl Default for PaymentSyncType { fn default() -> Self { let data = types::PaymentsSyncData {