diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 2ef30449b3..41b2c0f80e 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6663,6 +6663,7 @@ "signifyd", "plaid", "riskified", + "xendit", "zen", "zsl" ] @@ -19420,6 +19421,7 @@ "wise", "worldline", "worldpay", + "xendit", "zen", "plaid", "zsl" diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 4cc49cff45..c29c253b1f 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9316,6 +9316,7 @@ "signifyd", "plaid", "riskified", + "xendit", "zen", "zsl" ] @@ -23975,6 +23976,7 @@ "wise", "worldline", "worldpay", + "xendit", "zen", "plaid", "zsl" diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index e3a634eb4e..0402ae0645 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -130,7 +130,7 @@ pub enum RoutableConnectors { Wise, Worldline, Worldpay, - // Xendit, + Xendit, Zen, Plaid, Zsl, @@ -268,7 +268,7 @@ pub enum Connector { Signifyd, Plaid, Riskified, - // Xendit, + Xendit, Zen, Zsl, } @@ -404,7 +404,7 @@ impl Connector { | Self::Wise | Self::Worldline | Self::Worldpay - // | Self::Xendit + | Self::Xendit | Self::Zen | Self::Zsl | Self::Signifyd @@ -536,6 +536,7 @@ impl From for Connector { RoutableConnectors::Zen => Self::Zen, RoutableConnectors::Plaid => Self::Plaid, RoutableConnectors::Zsl => Self::Zsl, + RoutableConnectors::Xendit => Self::Xendit, } } } diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index e7d83ca14c..791a0679ea 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -414,6 +414,7 @@ impl ConnectorConfig { Connector::DummyConnector7 => Ok(connector_data.paypal_test), Connector::Netcetera => Ok(connector_data.netcetera), Connector::CtpMastercard => Ok(connector_data.ctp_mastercard), + Connector::Xendit => Ok(connector_data.xendit), } } } diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index c2d577dce9..2ab348d5dd 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -4472,3 +4472,43 @@ label="Merchant Country Code" placeholder="Enter Merchant Country Code" required=true type="Text" + +[xendit] +[[xendit.credit]] + payment_method_type = "Mastercard" +[[xendit.credit]] + payment_method_type = "Visa" +[[xendit.credit]] + payment_method_type = "Interac" +[[xendit.credit]] + payment_method_type = "AmericanExpress" +[[xendit.credit]] + payment_method_type = "JCB" +[[xendit.credit]] + payment_method_type = "DinersClub" +[[xendit.credit]] + payment_method_type = "Discover" +[[xendit.credit]] + payment_method_type = "CartesBancaires" +[[xendit.credit]] + payment_method_type = "UnionPay" +[[xendit.debit]] + payment_method_type = "Mastercard" +[[xendit.debit]] + payment_method_type = "Visa" +[[xendit.debit]] + payment_method_type = "Interac" +[[xendit.debit]] + payment_method_type = "AmericanExpress" +[[xendit.debit]] + payment_method_type = "JCB" +[[xendit.debit]] + payment_method_type = "DinersClub" +[[xendit.debit]] + payment_method_type = "Discover" +[[xendit.debit]] + payment_method_type = "CartesBancaires" +[[xendit.debit]] + payment_method_type = "UnionPay" +[xendit.connector_auth.HeaderKey] +api_key="API Key" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index aad91f830d..35f2f14c74 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -3340,4 +3340,44 @@ merchant_secret="Source verification key" [elavon.connector_auth.SignatureKey] api_key="Account Id" key1="User ID" -api_secret="Pin" \ No newline at end of file +api_secret="Pin" + +[xendit] +[[xendit.credit]] + payment_method_type = "Mastercard" +[[xendit.credit]] + payment_method_type = "Visa" +[[xendit.credit]] + payment_method_type = "Interac" +[[xendit.credit]] + payment_method_type = "AmericanExpress" +[[xendit.credit]] + payment_method_type = "JCB" +[[xendit.credit]] + payment_method_type = "DinersClub" +[[xendit.credit]] + payment_method_type = "Discover" +[[xendit.credit]] + payment_method_type = "CartesBancaires" +[[xendit.credit]] + payment_method_type = "UnionPay" +[[xendit.debit]] + payment_method_type = "Mastercard" +[[xendit.debit]] + payment_method_type = "Visa" +[[xendit.debit]] + payment_method_type = "Interac" +[[xendit.debit]] + payment_method_type = "AmericanExpress" +[[xendit.debit]] + payment_method_type = "JCB" +[[xendit.debit]] + payment_method_type = "DinersClub" +[[xendit.debit]] + payment_method_type = "Discover" +[[xendit.debit]] + payment_method_type = "CartesBancaires" +[[xendit.debit]] + payment_method_type = "UnionPay" +[xendit.connector_auth.HeaderKey] +api_key="API Key" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 7126b33b30..ed90381221 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -4409,3 +4409,43 @@ label="Merchant Country Code" placeholder="Enter Merchant Country Code" required=true type="Text" + +[xendit] +[[xendit.credit]] + payment_method_type = "Mastercard" +[[xendit.credit]] + payment_method_type = "Visa" +[[xendit.credit]] + payment_method_type = "Interac" +[[xendit.credit]] + payment_method_type = "AmericanExpress" +[[xendit.credit]] + payment_method_type = "JCB" +[[xendit.credit]] + payment_method_type = "DinersClub" +[[xendit.credit]] + payment_method_type = "Discover" +[[xendit.credit]] + payment_method_type = "CartesBancaires" +[[xendit.credit]] + payment_method_type = "UnionPay" +[[xendit.debit]] + payment_method_type = "Mastercard" +[[xendit.debit]] + payment_method_type = "Visa" +[[xendit.debit]] + payment_method_type = "Interac" +[[xendit.debit]] + payment_method_type = "AmericanExpress" +[[xendit.debit]] + payment_method_type = "JCB" +[[xendit.debit]] + payment_method_type = "DinersClub" +[[xendit.debit]] + payment_method_type = "Discover" +[[xendit.debit]] + payment_method_type = "CartesBancaires" +[[xendit.debit]] + payment_method_type = "UnionPay" +[xendit.connector_auth.HeaderKey] +api_key="API Key" \ No newline at end of file diff --git a/crates/hyperswitch_connectors/src/connectors/xendit.rs b/crates/hyperswitch_connectors/src/connectors/xendit.rs index 571bd87e83..dae6b8fd88 100644 --- a/crates/hyperswitch_connectors/src/connectors/xendit.rs +++ b/crates/hyperswitch_connectors/src/connectors/xendit.rs @@ -1,13 +1,16 @@ pub mod transformers; - +use base64::Engine; +use common_enums::{enums, CallConnectorAction, CaptureMethod, PaymentAction, PaymentMethodType}; use common_utils::{ + consts::BASE64_ENGINE, errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, FloatMajorUnit, FloatMajorUnitForConnector}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ + payment_method_data::PaymentMethodData, router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, @@ -21,39 +24,42 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, }, }; use hyperswitch_interfaces::{ api::{ - self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorSpecifications, - ConnectorValidation, + self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorRedirectResponse, + ConnectorSpecifications, ConnectorValidation, }, configs::Connectors, errors, events::connector_api_logs::ConnectorEvent, - types::{self, Response}, + types::{self, PaymentsAuthorizeType, PaymentsCaptureType, PaymentsSyncType, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{Mask, PeekInterface}; use transformers as xendit; -use crate::{constants::headers, types::ResponseRouterData, utils}; +use crate::{ + constants::headers, + types::ResponseRouterData, + utils::{self, PaymentMethodDataType, RefundsRequestData}, +}; #[derive(Clone)] pub struct Xendit { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Xendit { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &FloatMajorUnitForConnector, } } } - impl api::Payment for Xendit {} impl api::PaymentSession for Xendit {} impl api::ConnectorAccessToken for Xendit {} @@ -70,7 +76,6 @@ impl api::PaymentToken for Xendit {} impl ConnectorIntegration for Xendit { - // Not Implemented (R) } impl ConnectorCommonExt for Xendit @@ -99,9 +104,6 @@ impl ConnectorCommon for Xendit { 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,46 +120,81 @@ impl ConnectorCommon for Xendit { ) -> CustomResult)>, errors::ConnectorError> { let auth = xendit::XenditAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let encoded_api_key = BASE64_ENGINE.encode(format!("{}:", auth.api_key.peek())); Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + format!("Basic {encoded_api_key}").into_masked(), )]) } - fn build_error_response( &self, res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: xendit::XenditErrorResponse = res + let error_response: xendit::XenditErrorResponse = res .response - .parse_struct("XenditErrorResponse") + .parse_struct("ErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - + event_builder.map(|i| i.set_error_response_body(&error_response)); + router_env::logger::info!(connector_response=?error_response); Ok(ErrorResponse { + code: error_response.error_code.clone(), + message: error_response.message.clone(), + reason: Some(error_response.message.clone()), status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, attempt_status: None, connector_transaction_id: None, }) } } +impl ConnectorIntegration for Xendit { + fn build_request( + &self, + _req: &PaymentsCancelRouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotSupported { + message: "Cancel/Void flow".to_string(), + connector: "Xendit", + } + .into()) + } +} impl ConnectorValidation for Xendit { - //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 { + CaptureMethod::Automatic + | CaptureMethod::Manual + | CaptureMethod::SequentialAutomatic => Ok(()), + CaptureMethod::ManualMultiple | CaptureMethod::Scheduled => Err( + utils::construct_not_supported_error_report(capture_method, self.id()), + ), + } + } + fn validate_mandate_payment( + &self, + pm_type: Option, + pm_data: PaymentMethodData, + ) -> CustomResult<(), errors::ConnectorError> { + let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]); + utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id()) + } } +impl ConnectorIntegration for Xendit {} + impl ConnectorIntegration for Xendit { //TODO: implement sessions flow } -impl ConnectorIntegration for Xendit {} - impl ConnectorIntegration for Xendit {} impl ConnectorIntegration for Xendit { @@ -176,9 +213,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/payment_requests", self.base_url(connectors))) } fn get_request_body( @@ -191,9 +228,8 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: xendit::XenditPaymentsResponse = res + let response: xendit::XenditPaymentResponse = res .response - .parse_struct("Xendit PaymentsAuthorizeResponse") + .parse_struct("XenditPaymentResponse") .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(), @@ -262,10 +295,18 @@ impl ConnectorIntegration for Xen fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}/payment_requests/{connector_payment_id}", + self.base_url(connectors), + )) } fn build_request( @@ -276,9 +317,9 @@ impl ConnectorIntegration for Xen Ok(Some( RequestBuilder::new() .method(Method::Get) - .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .url(&PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .headers(PaymentsSyncType::get_headers(self, req, connectors)?) .build(), )) } @@ -289,25 +330,20 @@ impl ConnectorIntegration for Xen event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: xendit::XenditPaymentsResponse = res + let response: xendit::XenditPaymentResponse = res .response - .parse_struct("xendit PaymentsSyncResponse") + .parse_struct("xendit XenditPaymentResponse") .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( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) + .change_context(errors::ConnectorError::ResponseHandlingFailed) } } @@ -326,18 +362,42 @@ impl ConnectorIntegration fo fn get_url( &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}/payment_requests/{connector_payment_id}/captures", + self.base_url(connectors), + )) } 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_to_capture = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + let authorized_amount = utils::convert_amount( + self.amount_converter, + req.request.minor_payment_amount, + req.request.currency, + )?; + if amount_to_capture != authorized_amount { + Err(report!(errors::ConnectorError::NotSupported { + message: "Partial Capture".to_string(), + connector: "Xendit" + })) + } else { + let connector_router_data = xendit::XenditRouterData::from((amount_to_capture, req)); + let connector_req = + xendit::XenditPaymentsCaptureRequest::try_from(connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } } fn build_request( @@ -348,14 +408,10 @@ impl ConnectorIntegration fo Ok(Some( RequestBuilder::new() .method(Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .url(&PaymentsCaptureType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( - self, req, connectors, - )?) + .headers(PaymentsCaptureType::get_headers(self, req, connectors)?) + .set_body(self.get_request_body(req, connectors)?) .build(), )) } @@ -366,17 +422,20 @@ impl ConnectorIntegration fo event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: xendit::XenditPaymentsResponse = res + let response: xendit::XenditPaymentResponse = res .response - .parse_struct("Xendit PaymentsCaptureResponse") + .parse_struct("Xendit PaymentsResponse") .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, }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) } fn get_error_response( @@ -388,8 +447,6 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Xendit {} - impl ConnectorIntegration for Xendit { fn get_headers( &self, @@ -406,9 +463,9 @@ impl ConnectorIntegration for Xendit fn get_url( &self, _req: &RefundsRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/refunds", self.base_url(connectors),)) } fn get_request_body( @@ -416,13 +473,12 @@ impl ConnectorIntegration for Xendit req: &RefundsRouterData, _connectors: &Connectors, ) -> CustomResult { - let refund_amount = utils::convert_amount( + let amount = utils::convert_amount( self.amount_converter, req.request.minor_refund_amount, req.request.currency, )?; - - let connector_router_data = xendit::XenditRouterData::from((refund_amount, req)); + let connector_router_data = xendit::XenditRouterData::from((amount, req)); let connector_req = xendit::XenditRefundRequest::try_from(&connector_router_data)?; Ok(RequestContent::Json(Box::new(connector_req))) } @@ -455,14 +511,17 @@ impl ConnectorIntegration for Xendit let response: xendit::RefundResponse = res.response .parse_struct("xendit RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + 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, }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) } fn get_error_response( @@ -489,10 +548,15 @@ impl ConnectorIntegration for Xendit { fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_refund_id = req.request.get_connector_refund_id()?; + Ok(format!( + "{}/refunds/{}", + self.base_url(connectors), + connector_refund_id + )) } fn build_request( @@ -506,9 +570,6 @@ impl ConnectorIntegration for Xendit { .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .set_body(types::RefundSyncType::get_request_body( - self, req, connectors, - )?) .build(), )) } @@ -519,10 +580,11 @@ impl ConnectorIntegration for Xendit { event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: xendit::RefundResponse = res - .response - .parse_struct("xendit RefundSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: xendit::RefundResponse = + res.response + .parse_struct("xendit RefundResponse") + .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 { @@ -530,6 +592,7 @@ impl ConnectorIntegration for Xendit { data: data.clone(), http_code: res.status_code, }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) } fn get_error_response( @@ -566,3 +629,37 @@ impl webhooks::IncomingWebhook for Xendit { } impl ConnectorSpecifications for Xendit {} + +impl ConnectorRedirectResponse for Xendit { + fn get_flow_type( + &self, + _query_params: &str, + _json_payload: Option, + action: PaymentAction, + ) -> CustomResult { + match action { + PaymentAction::PSync + | PaymentAction::PaymentAuthenticateCompleteAuthorize + | PaymentAction::CompleteAuthorize => Ok(CallConnectorAction::Trigger), + // PaymentAction::CompleteAuthorize => { + // let parsed_query: Vec<(String, String)> = + // form_urlencoded::parse(query_params.as_bytes()) + // .into_owned() + // .collect(); + // let status = parsed_query + // .iter() + // .find(|(key, _)| key == "status") + // .map(|(_, value)| value.as_str()); + + // match status { + // Some("VERIFIED") => Ok(CallConnectorAction::Trigger), + // _ => Ok(CallConnectorAction::StatusUpdate { + // status: enums::AttemptStatus::AuthenticationFailed, + // error_code: Some("INVALID_STATUS".to_string()), + // error_message: Some("INVALID_STATUS".to_string()), + // }), + // } + // } + } + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs b/crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs index c9d4cd2583..8ff4b74c07 100644 --- a/crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs @@ -1,80 +1,450 @@ +use std::collections::HashMap; + +use cards::CardNumber; use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_utils::{pii, request::Method, types::FloatMajorUnit}; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + router_data::{ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + router_request_types::{PaymentsAuthorizeData, PaymentsCaptureData, ResponseId}, + router_response_types::{ + MandateReference, PaymentsResponseData, RedirectForm, RefundsResponseData, + }, + types::{ + PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, + RefundsRouterData, + }, }; -use hyperswitch_interfaces::errors; -use masking::Secret; +use hyperswitch_interfaces::{ + consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}, + errors, +}; +use masking::{ExposeInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + types::{PaymentsSyncResponseRouterData, RefundsResponseRouterData, ResponseRouterData}, + utils::{ + get_unimplemented_payment_method_error_message, CardData, PaymentsAuthorizeRequestData, + PaymentsSyncRequestData, RouterData as OtherRouterData, + }, }; //TODO: Fill the struct with respective fields pub struct XenditRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: FloatMajorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. pub router_data: T, } -impl From<(StringMinorUnit, T)> for XenditRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +#[derive(Serialize, Deserialize, Debug)] +pub enum PaymentMethodType { + CARD, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct ChannelProperties { + pub success_return_url: String, + pub failure_return_url: String, + pub skip_three_d_secure: bool, +} +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +pub enum PaymentMethod { + Card(CardPaymentRequest), +} +#[derive(Serialize, Deserialize, Debug)] +pub struct CardPaymentRequest { + #[serde(rename = "type")] + pub payment_type: PaymentMethodType, + pub card: CardInfo, + pub reusability: TransactionType, + pub reference_id: Secret, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct MandatePaymentRequest { + pub amount: FloatMajorUnit, + pub currency: common_enums::Currency, + pub capture_method: String, + pub payment_method_id: Secret, + pub channel_properties: ChannelProperties, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct XenditRedirectionResponse { + pub status: PaymentStatus, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct XenditPaymentsCaptureRequest { + pub capture_amount: FloatMajorUnit, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct XenditPaymentsRequest { + pub amount: FloatMajorUnit, + pub currency: common_enums::Currency, + pub capture_method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_method: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_method_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_properties: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct CardInfo { + pub channel_properties: ChannelProperties, + pub card_information: CardInformation, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct CardInformation { + pub card_number: CardNumber, + pub expiry_month: Secret, + pub expiry_year: Secret, + pub cvv: Secret, + pub cardholder_name: Secret, + pub cardholder_email: pii::Email, + pub cardholder_phone_number: Secret, +} +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TransactionType { + OneTimeUse, + MultipleUse, +} +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct XenditErrorResponse { + pub error_code: String, + pub message: String, +} +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PaymentStatus { + Pending, + RequiresAction, + Failed, + Succeeded, + AwaitingCapture, + Verified, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct XenditPaymentResponse { + pub id: String, + pub status: PaymentStatus, + pub actions: Option>, + pub payment_method: PaymentMethodInfo, + pub failure_code: Option, + pub reference_id: Secret, +} + +fn map_payment_response_to_attempt_status( + response: XenditPaymentResponse, + is_auto_capture: bool, +) -> enums::AttemptStatus { + match response.status { + PaymentStatus::Failed => enums::AttemptStatus::Failure, + PaymentStatus::Succeeded | PaymentStatus::Verified => { + if is_auto_capture { + enums::AttemptStatus::Charged + } else { + enums::AttemptStatus::Authorized + } + } + PaymentStatus::Pending => enums::AttemptStatus::Pending, + PaymentStatus::RequiresAction => enums::AttemptStatus::AuthenticationPending, + PaymentStatus::AwaitingCapture => enums::AttemptStatus::Authorized, + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum MethodType { + Get, + Post, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Action { + pub method: MethodType, + pub url: String, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentMethodInfo { + pub id: Secret, +} +impl TryFrom> for XenditPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: XenditRouterData<&PaymentsAuthorizeRouterData>) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(card_data) => Ok(Self { + capture_method: match item.router_data.request.is_auto_capture()? { + true => "AUTOMATIC".to_string(), + false => "MANUAL".to_string(), + }, + currency: item.router_data.request.currency, + amount: item.amount, + payment_method: Some(PaymentMethod::Card(CardPaymentRequest { + payment_type: PaymentMethodType::CARD, + reference_id: Secret::new( + item.router_data.connector_request_reference_id.clone(), + ), + card: CardInfo { + channel_properties: ChannelProperties { + success_return_url: item.router_data.request.get_router_return_url()?, + failure_return_url: item.router_data.request.get_router_return_url()?, + skip_three_d_secure: !item.router_data.is_three_ds(), + }, + card_information: CardInformation { + card_number: card_data.card_number.clone(), + expiry_month: card_data.card_exp_month.clone(), + expiry_year: card_data.get_expiry_year_4_digit(), + cvv: card_data.card_cvc.clone(), + cardholder_name: card_data + .get_cardholder_name() + .or(item.router_data.get_billing_full_name())?, + cardholder_email: item + .router_data + .get_billing_email() + .or(item.router_data.request.get_email())?, + cardholder_phone_number: item.router_data.get_billing_phone_number()?, + }, + }, + reusability: match item.router_data.request.is_mandate_payment() { + true => TransactionType::MultipleUse, + false => TransactionType::OneTimeUse, + }, + })), + payment_method_id: None, + channel_properties: None, + }), + PaymentMethodData::MandatePayment => Ok(Self { + channel_properties: Some(ChannelProperties { + success_return_url: item.router_data.request.get_router_return_url()?, + failure_return_url: item.router_data.request.get_router_return_url()?, + skip_three_d_secure: true, + }), + capture_method: match item.router_data.request.is_auto_capture()? { + true => "AUTOMATIC".to_string(), + false => "MANUAL".to_string(), + }, + currency: item.router_data.request.currency, + amount: item.amount, + payment_method_id: Some(Secret::new( + item.router_data.request.get_connector_mandate_id()?, + )), + payment_method: None, + }), + _ => Err(errors::ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("xendit"), + ) + .into()), + } + } +} +impl TryFrom> for XenditPaymentsCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: XenditRouterData<&PaymentsCaptureRouterData>) -> Result { + Ok(Self { + capture_amount: item.amount, + }) + } +} +impl + TryFrom< + ResponseRouterData, + > for RouterData +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData< + F, + XenditPaymentResponse, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + ) -> Result { + let status = map_payment_response_to_attempt_status( + item.response.clone(), + item.data.request.is_auto_capture()?, + ); + let response = if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: item + .response + .failure_code + .clone() + .unwrap_or_else(|| NO_ERROR_CODE.to_string()), + message: item + .response + .failure_code + .clone() + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + reason: Some( + item.response + .failure_code + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + ), + attempt_status: None, + connector_transaction_id: Some(item.response.id.clone()), + status_code: item.http_code, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.id.clone()), + redirection_data: match item.response.actions { + Some(actions) if !actions.is_empty() => { + actions.first().map_or(Box::new(None), |single_action| { + Box::new(Some(RedirectForm::Form { + endpoint: single_action.url.clone(), + method: match single_action.method { + MethodType::Get => Method::Get, + MethodType::Post => Method::Post, + }, + form_fields: HashMap::new(), + })) + }) + } + _ => Box::new(None), + }, + mandate_reference: match item.data.request.is_mandate_payment() { + true => Box::new(Some(MandateReference { + connector_mandate_id: Some(item.response.payment_method.id.expose()), + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + })), + false => Box::new(None), + }, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + item.response.reference_id.peek().to_string(), + ), + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData< + F, + XenditPaymentResponse, + PaymentsCaptureData, + PaymentsResponseData, + >, + ) -> Result { + let status = map_payment_response_to_attempt_status(item.response.clone(), true); + let response = if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: item + .response + .failure_code + .clone() + .unwrap_or_else(|| NO_ERROR_CODE.to_string()), + message: item + .response + .failure_code + .clone() + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + reason: Some( + item.response + .failure_code + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + ), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::NoResponseId, + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + item.response.reference_id.peek().to_string(), + ), + incremental_authorization_allowed: None, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} +impl TryFrom> for PaymentsSyncRouterData { + type Error = error_stack::Report; + fn try_from( + item: PaymentsSyncResponseRouterData, + ) -> Result { + let status = map_payment_response_to_attempt_status( + item.response.clone(), + item.data.request.is_auto_capture()?, + ); + let response = if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: item + .response + .failure_code + .clone() + .unwrap_or_else(|| NO_ERROR_CODE.to_string()), + message: item + .response + .failure_code + .clone() + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + reason: Some( + item.response + .failure_code + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + ), + attempt_status: None, + connector_transaction_id: Some(item.response.id.clone()), + status_code: item.http_code, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::NoResponseId, + 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, + charge_id: None, + }) + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} +impl From<(FloatMajorUnit, T)> for XenditRouterData { + fn from((amount, item): (FloatMajorUnit, T)) -> Self { Self { amount, router_data: item, } } } - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] -pub struct XenditPaymentsRequest { - amount: StringMinorUnit, - card: XenditCard, -} - -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct XenditCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, -} - -impl TryFrom<&XenditRouterData<&PaymentsAuthorizeRouterData>> for XenditPaymentsRequest { - type Error = error_stack::Report; - fn try_from( - item: &XenditRouterData<&PaymentsAuthorizeRouterData>, - ) -> Result { - match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = XenditCard { - 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()?, - }; - Ok(Self { - amount: item.amount.clone(), - card, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), - } - } -} - -//TODO: Fill the struct with respective fields -// Auth Struct pub struct XenditAuthType { pub(super) api_key: Secret, } @@ -90,64 +460,15 @@ impl TryFrom<&ConnectorAuthType> for XenditAuthType { } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum XenditPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for common_enums::AttemptStatus { - fn from(item: XenditPaymentStatus) -> Self { - match item { - XenditPaymentStatus::Succeeded => Self::Charged, - XenditPaymentStatus::Failed => Self::Failure, - XenditPaymentStatus::Processing => Self::Authorizing, - } - } -} - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct XenditPaymentsResponse { - status: XenditPaymentStatus, - id: String, -} - -impl TryFrom> - for RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: ResponseRouterData, - ) -> Result { - Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.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, - charge_id: None, - }), - ..item.data - }) - } -} //TODO: Fill the struct with respective fields // REFUND : // Type definition for RefundRequest #[derive(Default, Debug, Serialize)] pub struct XenditRefundRequest { - pub amount: StringMinorUnit, + pub amount: FloatMajorUnit, + pub payment_request_id: String, + pub reason: String, } impl TryFrom<&XenditRouterData<&RefundsRouterData>> for XenditRefundRequest { @@ -155,34 +476,34 @@ impl TryFrom<&XenditRouterData<&RefundsRouterData>> for XenditRefundReques fn try_from(item: &XenditRouterData<&RefundsRouterData>) -> Result { Ok(Self { amount: item.amount.to_owned(), + payment_request_id: item.router_data.request.connector_transaction_id.clone(), + reason: "REQUESTED_BY_CUSTOMER".to_string(), }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum RefundStatus { + RequiresAction, Succeeded, Failed, - #[default] - Processing, + Pending, + Cancelled, } impl From for enums::RefundStatus { fn from(item: RefundStatus) -> Self { match item { RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + RefundStatus::Failed | RefundStatus::Cancelled => Self::Failure, + RefundStatus::Pending | RefundStatus::RequiresAction => Self::Pending, } } } //TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefundResponse { id: String, status: RefundStatus, @@ -195,7 +516,7 @@ impl TryFrom> for RefundsRout ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), + connector_refund_id: item.response.id, refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data @@ -217,12 +538,3 @@ impl TryFrom> for RefundsRouter }) } } - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct XenditErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, -} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 703b0bb2ac..0e752a2937 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -625,7 +625,6 @@ default_imp_for_connector_redirect_response!( connectors::UnifiedAuthenticationService, connectors::Worldline, connectors::Volt, - connectors::Xendit, connectors::Zsl, connectors::CtpMastercard ); diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 169e5ae146..140fcb32af 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -946,6 +946,7 @@ pub trait CardData { fn get_expiry_month_as_i8(&self) -> Result, Error>; fn get_expiry_year_as_i32(&self) -> Result, Error>; fn get_expiry_year_as_4_digit_i32(&self) -> Result, Error>; + fn get_cardholder_name(&self) -> Result, Error>; } impl CardData for Card { @@ -1032,6 +1033,11 @@ impl CardData for Card { .change_context(errors::ConnectorError::ResponseDeserializationFailed) .map(Secret::new) } + fn get_cardholder_name(&self) -> Result, Error> { + self.card_holder_name + .clone() + .ok_or_else(missing_field_err("card.card_holder_name")) + } } #[track_caller] 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 25bc4db463..1d2be85ba1 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -3384,6 +3384,72 @@ impl Default for settings::RequiredFields { }, } ), + ( + enums::Connector::Xendit, + 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, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "payment_method_data.billing.phone.number".to_string(), + RequiredFieldInfo { + required_field: "billing.phone.number".to_string(), + display_name: "phone_number".to_string(), + field_type: enums::FieldType::UserPhoneNumber, + value: None, + } + ) + + ] + ), + } + ), ( enums::Connector::Zen, RequiredFieldFinal { @@ -6703,6 +6769,72 @@ impl Default for settings::RequiredFields { }, } ), + ( + enums::Connector::Xendit, + 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, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "payment_method_data.billing.phone.number".to_string(), + RequiredFieldInfo { + required_field: "billing.phone.number".to_string(), + display_name: "phone_number".to_string(), + field_type: enums::FieldType::UserPhoneNumber, + value: None, + } + ) + + ] + ), + } + ), ( enums::Connector::Zen, RequiredFieldFinal { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 35df1ca8df..fd9f08089a 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1550,6 +1550,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { worldpay::transformers::WorldpayAuthType::try_from(self.auth_type)?; Ok(()) } + api_enums::Connector::Xendit => { + xendit::transformers::XenditAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Zen => { zen::transformers::ZenAuthType::try_from(self.auth_type)?; Ok(()) diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index c96b8f915b..697ec6fbdf 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -2133,6 +2133,7 @@ default_imp_for_new_connector_integration_connector_authentication!( connector::Wise, connector::Worldline, connector::Worldpay, + connector::Xendit, connector::Zen, connector::Zsl, connector::Plaid diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 9c9d0a454b..925fb0f12b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -507,6 +507,7 @@ default_imp_for_connector_request_id!( connector::Wise, connector::Worldline, connector::Worldpay, + connector::Xendit, connector::Zen, connector::Zsl, connector::CtpMastercard @@ -1644,6 +1645,7 @@ default_imp_for_fraud_check!( connector::Wise, connector::Worldline, connector::Worldpay, + connector::Xendit, connector::Zen, connector::Zsl, connector::CtpMastercard @@ -2248,6 +2250,7 @@ default_imp_for_connector_authentication!( connector::Wise, connector::Worldline, connector::Worldpay, + connector::Xendit, connector::Zen, connector::Zsl, connector::CtpMastercard diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index b0c3d2d21d..243b6116e7 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -511,9 +511,9 @@ impl ConnectorData { enums::Connector::Worldpay => { Ok(ConnectorEnum::Old(Box::new(connector::Worldpay::new()))) } - // enums::Connector::Xendit => { - // Ok(ConnectorEnum::Old(Box::new(connector::Xendit::new()))) - // } + enums::Connector::Xendit => { + Ok(ConnectorEnum::Old(Box::new(connector::Xendit::new()))) + } enums::Connector::Mifinity => { Ok(ConnectorEnum::Old(Box::new(connector::Mifinity::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index cb7d3f78f7..06682f596e 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -313,7 +313,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Wise => Self::Wise, api_enums::Connector::Worldline => Self::Worldline, api_enums::Connector::Worldpay => Self::Worldpay, - // api_enums::Connector::Xendit => Self::Xendit, + api_enums::Connector::Xendit => Self::Xendit, api_enums::Connector::Zen => Self::Zen, api_enums::Connector::Zsl => Self::Zsl, #[cfg(feature = "dummy_connector")] diff --git a/cypress-tests/cypress/e2e/PaymentTest/00007-VoidPayment.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00007-VoidPayment.cy.js index 0050aa39bf..68582a8e9a 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00007-VoidPayment.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00007-VoidPayment.cy.js @@ -54,6 +54,13 @@ describe("Card - NoThreeDS Manual payment flow test", () => { if (shouldContinue) shouldContinue = utils.should_continue_further(data); }); + it("retrieve-payment-call-test", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["No3DSManualCapture"]; + + cy.retrievePaymentCallTest(globalState, data); + }); it("void-call-test", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" @@ -149,6 +156,13 @@ describe("Card - NoThreeDS Manual payment flow test", () => { if (shouldContinue) shouldContinue = utils.should_continue_further(data); }); + it("retrieve-payment-call-test", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["No3DSManualCapture"]; + + cy.retrievePaymentCallTest(globalState, data); + }); it("void-call-test", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index e756c0593d..011383e887 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -27,6 +27,7 @@ import { connectorDetails as trustpayConnectorDetails } from "./Trustpay.js"; import { connectorDetails as wellsfargoConnectorDetails } from "./WellsFargo.js"; import { connectorDetails as worldpayConnectorDetails } from "./WorldPay.js"; import { connectorDetails as deutschebankConnectorDetails } from "./Deutschebank.js"; +import { connectorDetails as xenditConnectorDetails } from "./Xendit.js"; const connectorDetails = { adyen: adyenConnectorDetails, @@ -44,6 +45,7 @@ const connectorDetails = { nmi: nmiConnectorDetails, novalnet: novalnetConnectorDetails, paybox: payboxConnectorDetails, + xendit: xenditConnectorDetails, paypal: paypalConnectorDetails, stripe: stripeConnectorDetails, elavon: elavonConnectorDetails, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Xendit.js b/cypress-tests/cypress/e2e/PaymentUtils/Xendit.js new file mode 100644 index 0000000000..fcc303877b --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Xendit.js @@ -0,0 +1,624 @@ +const successfulNo3DSCardDetails = { + card_number: "4000000000001091", + card_exp_month: "12", + card_exp_year: "27", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; +const billingDetails = { + email: "mauro.morandi@nexi.it", + phone: { + number: "9123456789", + country_code: "+91", + }, +}; +const customerAcceptance = { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, +}; +const paymentMethodData3ds = { + card: { + last4: "1091", + card_type: "CREDIT", + card_network: "Visa", + card_issuer: "INTL HDQTRS-CENTER OWNED", + card_issuing_country: "UNITEDSTATES", + card_isin: "400000", + card_extended_bin: null, + card_exp_month: "12", + card_exp_year: "27", + card_holder_name: "joseph Doe", + payment_checks: null, + authentication_data: null, + }, + billing: { + address: null, + email: "mauro.morandi@nexi.it", + phone: { + number: "9123456789", + country_code: "+91", + }, + }, +}; + +const singleUseMandateData = { + customer_acceptance: customerAcceptance, + mandate_type: { + single_use: { + amount: 1600000, + currency: "IDR", + }, + }, +}; + +const multiUseMandateData = { + customer_acceptance: customerAcceptance, + mandate_type: { + multi_use: { + amount: 8000, + currency: "IDR", + }, + }, +}; +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + currency: "IDR", + customer_acceptance: null, + setup_future_usage: "on_session", + amount: 6500000, + billing: billingDetails, + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + PaymentIntentWithShippingCost: { + Request: { + currency: "IDR", + shipping_cost: 100, + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + shipping_cost: 100, + amount: 6500000, + }, + }, + }, + PaymentConfirmWithShippingCost: { + Request: { + amount: 6500000, + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "processing", + shipping_cost: 100, + amount: 6500000, + }, + }, + }, + No3DSManualCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + amount: 6500000, + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + No3DSAutoCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + amount: 6500000, + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + manualPaymentPartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + manualPaymentRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + "3DSAutoCapture": { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + setup_future_usage: "on_session", + payment_method_data: paymentMethodData3ds, + }, + }, + }, + MandateMultiUseNo3DSAutoCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSManualCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + SaveCardUseNo3DSAutoCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + setup_future_usage: "on_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSAutoCaptureOffSession: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_type: "debit", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + setup_future_usage: "off_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSManualCaptureOffSession: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + setup_future_usage: "off_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + SaveCardConfirmAutoCaptureOffSession: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardConfirmManualCaptureOffSession: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + SaveCardUseNo3DSManualCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + setup_future_usage: "on_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + MandateSingleUseNo3DSAutoCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 1000, + }, + }, + Request: { + payment_method: "card", + amount: 6500000, + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + "3DSManualCapture": { + Request: { + amount: 6500000, + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + setup_future_usage: "on_session", + payment_method_data: paymentMethodData3ds, + }, + }, + }, + MandateSingleUseNo3DSManualCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + amount: 6500000, + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: billingDetails, + }, + currency: "IDR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + Capture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + amount: 6500000, + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6500000, + amount_capturable: 0, + amount_received: 6500000, + }, + }, + }, + PartialCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + amount: 6500000, + }, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6500000, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + Refund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + VoidAfterConfirm: { + Request: {}, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: "Cancel/Void flow is not supported", + code: "IR_19", + }, + }, + }, + }, + PartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SyncRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSAutoCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "IDR", + billing: billingDetails, + mandate_data: null, + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSManualCapture: { + Configs: { + DELAY: { + STATUS: true, + TIMEOUT: 3000, + }, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + billing: billingDetails, + currency: "IDR", + mandate_data: null, + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/support/redirectionHandler.js b/cypress-tests/cypress/support/redirectionHandler.js index a7edf8fb97..e6d1f21a14 100644 --- a/cypress-tests/cypress/support/redirectionHandler.js +++ b/cypress-tests/cypress/support/redirectionHandler.js @@ -309,7 +309,11 @@ function threeDsRedirection(redirection_url, expected_url, connectorId) { cy.get("#txtButton").click(); }); }); - } else if (connectorId === "nmi" || connectorId === "noon") { + } else if ( + connectorId === "nmi" || + connectorId === "noon" || + connectorId == "xendit" + ) { cy.get("iframe", { timeout: TIMEOUT }) .its("0.contentDocument.body") .within(() => {