mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feature(connector): add support for worldline connector (#374)
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3015,6 +3015,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"redis_interface",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"router_derive",
|
||||
|
||||
@ -93,6 +93,9 @@ base_url = "https://secure.snd.payu.com/api/"
|
||||
[connectors.globalpay]
|
||||
base_url = "https://apis.sandbox.globalpay.com/ucp/"
|
||||
|
||||
[connectors.worldline]
|
||||
base_url = "https://eu.sandbox.api-ingenico.com/"
|
||||
|
||||
[scheduler]
|
||||
stream = "SCHEDULER_STREAM"
|
||||
consumer_group = "SCHEDULER_GROUP"
|
||||
|
||||
@ -509,6 +509,7 @@ pub enum Connector {
|
||||
Payu,
|
||||
Shift4,
|
||||
Stripe,
|
||||
Worldline,
|
||||
Worldpay,
|
||||
}
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ nanoid = "0.4.0"
|
||||
num_cpus = "1.15.0"
|
||||
once_cell = "1.17.0"
|
||||
rand = "0.8.5"
|
||||
regex = "1.7.1"
|
||||
reqwest = { version = "0.11.13", features = ["json", "native-tls", "gzip"] }
|
||||
ring = "0.16.20"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
|
||||
@ -134,6 +134,7 @@ pub struct Connectors {
|
||||
pub shift4: ConnectorParams,
|
||||
pub stripe: ConnectorParams,
|
||||
pub supported: SupportedConnectors,
|
||||
pub worldline: ConnectorParams,
|
||||
pub worldpay: ConnectorParams,
|
||||
}
|
||||
|
||||
|
||||
@ -12,11 +12,12 @@ pub mod payu;
|
||||
pub mod shift4;
|
||||
pub mod stripe;
|
||||
pub mod utils;
|
||||
pub mod worldline;
|
||||
pub mod worldpay;
|
||||
|
||||
pub use self::{
|
||||
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
||||
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, fiserv::Fiserv,
|
||||
globalpay::Globalpay, klarna::Klarna, payu::Payu, shift4::Shift4, stripe::Stripe,
|
||||
worldpay::Worldpay,
|
||||
worldline::Worldline, worldpay::Worldpay,
|
||||
};
|
||||
|
||||
606
crates/router/src/connector/worldline.rs
Normal file
606
crates/router/src/connector/worldline.rs
Normal file
@ -0,0 +1,606 @@
|
||||
mod transformers;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use base64::Engine;
|
||||
use bytes::Bytes;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use ring::hmac;
|
||||
use time::{format_description, OffsetDateTime};
|
||||
use transformers as worldline;
|
||||
|
||||
use crate::{
|
||||
configs::settings::Connectors,
|
||||
consts,
|
||||
core::errors::{self, CustomResult},
|
||||
headers, logger,
|
||||
services::{self, ConnectorIntegration},
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCommon},
|
||||
ErrorResponse, Response,
|
||||
},
|
||||
utils::{self, BytesExt, OptionExt},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Worldline;
|
||||
|
||||
impl Worldline {
|
||||
pub fn generate_authorization_token(
|
||||
&self,
|
||||
auth: worldline::AuthType,
|
||||
http_method: &services::Method,
|
||||
content_type: &str,
|
||||
date: &str,
|
||||
endpoint: &str,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let signature_data: String = format!(
|
||||
"{}\n{}\n{}\n/{}\n",
|
||||
http_method,
|
||||
content_type.trim(),
|
||||
date.trim(),
|
||||
endpoint.trim()
|
||||
);
|
||||
let worldline::AuthType {
|
||||
api_key,
|
||||
api_secret,
|
||||
..
|
||||
} = auth;
|
||||
let key = hmac::Key::new(hmac::HMAC_SHA256, api_secret.as_bytes());
|
||||
let signed_data = consts::BASE64_ENGINE.encode(hmac::sign(&key, signature_data.as_bytes()));
|
||||
|
||||
Ok(format!("GCS v1HMAC:{api_key}:{signed_data}"))
|
||||
}
|
||||
|
||||
pub fn get_current_date_time() -> CustomResult<String, errors::ConnectorError> {
|
||||
let format = format_description::parse(
|
||||
"[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT",
|
||||
)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::InvalidDateFormat)?;
|
||||
OffsetDateTime::now_utc()
|
||||
.format(&format)
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::InvalidDateFormat)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorCommon for Worldline {
|
||||
fn id(&self) -> &'static str {
|
||||
"worldline"
|
||||
}
|
||||
|
||||
fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str {
|
||||
connectors.worldline.base_url.as_ref()
|
||||
}
|
||||
|
||||
fn build_error_response(
|
||||
&self,
|
||||
res: Bytes,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
let response: worldline::ErrorResponse = res
|
||||
.parse_struct("Worldline ErrorResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
let error = response.errors.into_iter().next().unwrap_or_default();
|
||||
Ok(ErrorResponse {
|
||||
code: error
|
||||
.code
|
||||
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()),
|
||||
message: error
|
||||
.message
|
||||
.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Payment for Worldline {}
|
||||
|
||||
impl api::PreVerify for Worldline {}
|
||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
}
|
||||
|
||||
impl api::PaymentVoid for Worldline {}
|
||||
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RouterData<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let base_url = self.base_url(connectors);
|
||||
let url = &types::PaymentsVoidType::get_url(self, req, connectors)?;
|
||||
let endpoint = url.clone().replace(base_url, "");
|
||||
let http_method = services::Method::Post;
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let date = Self::get_current_date_time()?;
|
||||
let content_type = types::PaymentsAuthorizeType::get_content_type(self);
|
||||
let signed_data: String =
|
||||
self.generate_authorization_token(auth, &http_method, content_type, &date, &endpoint)?;
|
||||
|
||||
Ok(vec![
|
||||
(headers::DATE.to_string(), date),
|
||||
(headers::AUTHORIZATION.to_string(), signed_data),
|
||||
(headers::CONTENT_TYPE.to_string(), content_type.to_string()),
|
||||
])
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
"application/json"
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::PaymentsCancelRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let base_url = self.base_url(connectors);
|
||||
let auth: worldline::AuthType = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let merchat_account_id = auth.merchant_account_id;
|
||||
let payment_id: &str = req.request.connector_transaction_id.as_ref();
|
||||
Ok(format!(
|
||||
"{base_url}v1/{merchat_account_id}/payments/{payment_id}/cancel"
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RouterData<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
|
||||
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCancelRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
|
||||
let response: worldline::PaymentResponse = res
|
||||
.response
|
||||
.parse_struct("PaymentResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
logger::debug!(payments_cancel_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<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentSync for Worldline {}
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let base_url = self.base_url(connectors);
|
||||
let url = &types::PaymentsSyncType::get_url(self, req, connectors)?;
|
||||
let endpoint = url.clone().replace(base_url, "");
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let date = Self::get_current_date_time()?;
|
||||
let signed_data: String =
|
||||
self.generate_authorization_token(auth, &services::Method::Get, "", &date, &endpoint)?;
|
||||
Ok(vec![
|
||||
(headers::DATE.to_string(), date),
|
||||
(headers::AUTHORIZATION.to_string(), signed_data),
|
||||
])
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let payment_id = req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
let base_url = self.base_url(connectors);
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let merchat_account_id = auth.merchant_account_id;
|
||||
Ok(format!(
|
||||
"{base_url}v1/{merchat_account_id}/payments/{payment_id}"
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<services::Request>, 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 get_error_response(
|
||||
&self,
|
||||
res: Bytes,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
logger::debug!(payment_sync_response=?res);
|
||||
let response: worldline::Payment = res
|
||||
.response
|
||||
.parse_struct("Payment")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentCapture for Worldline {}
|
||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
// Not Implemented
|
||||
}
|
||||
|
||||
impl api::PaymentSession for Worldline {}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
// Not Implemented
|
||||
}
|
||||
|
||||
impl api::PaymentAuthorize for Worldline {}
|
||||
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RouterData<
|
||||
api::Authorize,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let base_url = self.base_url(connectors);
|
||||
let url = &types::PaymentsAuthorizeType::get_url(self, req, connectors)?;
|
||||
let endpoint = url.clone().replace(base_url, "");
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let date = Self::get_current_date_time()?;
|
||||
let content_type = types::PaymentsAuthorizeType::get_content_type(self);
|
||||
let signed_data: String = self.generate_authorization_token(
|
||||
auth,
|
||||
&services::Method::Post,
|
||||
content_type,
|
||||
&date,
|
||||
&endpoint,
|
||||
)?;
|
||||
|
||||
Ok(vec![
|
||||
(headers::DATE.to_string(), date),
|
||||
(headers::AUTHORIZATION.to_string(), signed_data),
|
||||
(headers::CONTENT_TYPE.to_string(), content_type.to_string()),
|
||||
])
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
"application/json"
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let base_url = self.base_url(connectors);
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let merchat_account_id = auth.merchant_account_id;
|
||||
Ok(format!("{base_url}v1/{merchat_account_id}/payments"))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let worldline_req = utils::Encode::<worldline::PaymentsRequest>::convert_and_encode(req)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(worldline_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RouterData<
|
||||
api::Authorize,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<services::Request>, 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<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: worldline::PaymentResponse = res
|
||||
.response
|
||||
.parse_struct("PaymentIntentResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
logger::debug!(worldlinepayments_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<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Refund for Worldline {}
|
||||
impl api::RefundExecute for Worldline {}
|
||||
impl api::RefundSync for Worldline {}
|
||||
|
||||
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let base_url = self.base_url(connectors);
|
||||
let url = &types::RefundExecuteType::get_url(self, req, connectors)?;
|
||||
let endpoint = url.clone().replace(base_url, "");
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let date = Self::get_current_date_time()?;
|
||||
let content_type = types::RefundExecuteType::get_content_type(self);
|
||||
let signed_data: String = self.generate_authorization_token(
|
||||
auth,
|
||||
&services::Method::Post,
|
||||
content_type,
|
||||
&date,
|
||||
&endpoint,
|
||||
)?;
|
||||
|
||||
Ok(vec![
|
||||
(headers::DATE.to_string(), date),
|
||||
(headers::AUTHORIZATION.to_string(), signed_data),
|
||||
(headers::CONTENT_TYPE.to_string(), content_type.to_string()),
|
||||
])
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
"application/json"
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let payment_id = req.request.connector_transaction_id.clone();
|
||||
let base_url = self.base_url(connectors);
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let merchat_account_id = auth.merchant_account_id;
|
||||
Ok(format!(
|
||||
"{base_url}v1/{merchat_account_id}/payments/{payment_id}/refund"
|
||||
))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let refund_req =
|
||||
utils::Encode::<worldline::WorldlineRefundRequest>::convert_and_encode(req)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(refund_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<services::Request>, 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<api::Execute>,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
|
||||
logger::debug!(target: "router::connector::worldline", response=?res);
|
||||
let response: worldline::RefundResponse = res
|
||||
.response
|
||||
.parse_struct("worldline 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<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
|
||||
for Worldline
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let base_url = self.base_url(connectors);
|
||||
let url = &types::RefundSyncType::get_url(self, req, connectors)?;
|
||||
let endpoint = url.clone().replace(base_url, "");
|
||||
let auth = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let date = Self::get_current_date_time()?;
|
||||
let signed_data: String =
|
||||
self.generate_authorization_token(auth, &services::Method::Get, "", &date, &endpoint)?;
|
||||
|
||||
Ok(vec![
|
||||
(headers::DATE.to_string(), date),
|
||||
(headers::AUTHORIZATION.to_string(), signed_data),
|
||||
])
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let refund_id = req
|
||||
.response
|
||||
.as_ref()
|
||||
.ok()
|
||||
.get_required_value("response")
|
||||
.change_context(errors::ConnectorError::FailedToObtainIntegrationUrl)?
|
||||
.connector_refund_id
|
||||
.clone();
|
||||
let base_url = self.base_url(connectors);
|
||||
let auth: worldline::AuthType = worldline::AuthType::try_from(&req.connector_auth_type)?;
|
||||
let merchat_account_id = auth.merchant_account_id;
|
||||
Ok(format!(
|
||||
"{base_url}v1/{merchat_account_id}/refunds/{refund_id}/"
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &Connectors,
|
||||
) -> CustomResult<Option<services::Request>, 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<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||
logger::debug!(target: "router::connector::worldline", response=?res);
|
||||
let response: worldline::RefundResponse = res
|
||||
.response
|
||||
.parse_struct("worldline 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<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl api::IncomingWebhook for Worldline {
|
||||
fn get_webhook_object_reference_id(
|
||||
&self,
|
||||
_body: &[u8],
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
}
|
||||
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
_body: &[u8],
|
||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
}
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
&self,
|
||||
_body: &[u8],
|
||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
}
|
||||
}
|
||||
|
||||
impl services::ConnectorRedirectResponse for Worldline {}
|
||||
487
crates/router/src/connector/worldline/transformers.rs
Normal file
487
crates/router/src/connector/worldline/transformers.rs
Normal file
@ -0,0 +1,487 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use api_models::payments as api_models;
|
||||
use common_utils::pii::{self, Email};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{PeekInterface, Secret};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
core::errors,
|
||||
types::{self, api, storage::enums},
|
||||
};
|
||||
|
||||
static CARD_REGEX: Lazy<HashMap<CardProduct, Result<Regex, regex::Error>>> = Lazy::new(|| {
|
||||
let mut map = HashMap::new();
|
||||
// Reference: https://gist.github.com/michaelkeevildown/9096cd3aac9029c4e6e05588448a8841
|
||||
// [#379]: Determine card issuer from card BIN number
|
||||
map.insert(CardProduct::Master, Regex::new(r"^5[1-5][0-9]{14}$"));
|
||||
map.insert(
|
||||
CardProduct::AmericanExpress,
|
||||
Regex::new(r"^3[47][0-9]{13}$"),
|
||||
);
|
||||
map.insert(CardProduct::Visa, Regex::new(r"^4[0-9]{12}(?:[0-9]{3})?$"));
|
||||
map.insert(CardProduct::Discover, Regex::new(r"^65[4-9][0-9]{13}|64[4-9][0-9]{13}|6011[0-9]{12}|(622(?:12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9[01][0-9]|92[0-5])[0-9]{10})$"));
|
||||
map
|
||||
});
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Card {
|
||||
pub card_number: Secret<String, pii::CardNumber>,
|
||||
pub cardholder_name: Secret<String>,
|
||||
pub cvv: Secret<String>,
|
||||
pub expiry_date: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CardPaymentMethod {
|
||||
pub card: Card,
|
||||
pub requires_approval: bool,
|
||||
pub payment_product_id: u16,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AmountOfMoney {
|
||||
pub amount: i64,
|
||||
pub currency_code: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Order {
|
||||
pub amount_of_money: AmountOfMoney,
|
||||
pub customer: Customer,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BillingAddress {
|
||||
pub city: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
pub house_number: Option<String>,
|
||||
pub state: Option<Secret<String>>,
|
||||
pub state_code: Option<String>,
|
||||
pub street: Option<String>,
|
||||
pub zip: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContactDetails {
|
||||
pub email_address: Option<Secret<String, Email>>,
|
||||
pub mobile_phone_number: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Customer {
|
||||
pub billing_address: BillingAddress,
|
||||
pub contact_details: Option<ContactDetails>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Name {
|
||||
pub first_name: Option<Secret<String>>,
|
||||
pub surname: Option<Secret<String>>,
|
||||
pub surname_prefix: Option<Secret<String>>,
|
||||
pub title: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Shipping {
|
||||
pub city: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
pub house_number: Option<String>,
|
||||
pub name: Option<Name>,
|
||||
pub state: Option<Secret<String>>,
|
||||
pub state_code: Option<String>,
|
||||
pub street: Option<String>,
|
||||
pub zip: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaymentsRequest {
|
||||
pub card_payment_method_specific_input: CardPaymentMethod,
|
||||
pub order: Order,
|
||||
pub shipping: Option<Shipping>,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
match item.request.payment_method_data {
|
||||
api::PaymentMethod::Card(ref card) => {
|
||||
make_card_request(&item.address, &item.request, card)
|
||||
}
|
||||
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_card_request(
|
||||
address: &types::PaymentAddress,
|
||||
req: &types::PaymentsAuthorizeData,
|
||||
ccard: &api_models::CCard,
|
||||
) -> Result<PaymentsRequest, error_stack::Report<errors::ConnectorError>> {
|
||||
let card_number = ccard.card_number.peek().as_ref();
|
||||
let expiry_year = ccard.card_exp_year.peek().clone();
|
||||
let secret_value = format!("{}{}", ccard.card_exp_month.peek(), &expiry_year[2..]);
|
||||
let expiry_date: Secret<String> = Secret::new(secret_value);
|
||||
let card = Card {
|
||||
card_number: ccard.card_number.clone(),
|
||||
cardholder_name: ccard.card_holder_name.clone(),
|
||||
cvv: ccard.card_cvc.clone(),
|
||||
expiry_date,
|
||||
};
|
||||
let payment_product_id = get_card_product_id(card_number)?;
|
||||
let card_payment_method_specific_input = CardPaymentMethod {
|
||||
card,
|
||||
requires_approval: matches!(req.capture_method, Some(enums::CaptureMethod::Manual)),
|
||||
payment_product_id,
|
||||
};
|
||||
|
||||
let customer = build_customer_info(address, &req.email)?;
|
||||
|
||||
let order = Order {
|
||||
amount_of_money: AmountOfMoney {
|
||||
amount: req.amount,
|
||||
currency_code: req.currency.to_string().to_uppercase(),
|
||||
},
|
||||
customer,
|
||||
};
|
||||
|
||||
let shipping = address
|
||||
.shipping
|
||||
.as_ref()
|
||||
.and_then(|shipping| shipping.address.clone())
|
||||
.map(|address| Shipping { ..address.into() });
|
||||
|
||||
Ok(PaymentsRequest {
|
||||
card_payment_method_specific_input,
|
||||
order,
|
||||
shipping,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_card_product_id(
|
||||
card_number: &str,
|
||||
) -> Result<u16, error_stack::Report<errors::ConnectorError>> {
|
||||
for (k, v) in CARD_REGEX.iter() {
|
||||
let regex: Regex = v
|
||||
.clone()
|
||||
.into_report()
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
if regex.is_match(card_number) {
|
||||
return Ok(k.product_id());
|
||||
}
|
||||
}
|
||||
Err(error_stack::Report::new(
|
||||
errors::ConnectorError::RequestEncodingFailed,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_address(
|
||||
payment_address: &types::PaymentAddress,
|
||||
) -> Option<(&api_models::Address, &api_models::AddressDetails)> {
|
||||
let billing = payment_address.billing.as_ref()?;
|
||||
let address = billing.address.as_ref()?;
|
||||
address.country.as_ref()?;
|
||||
Some((billing, address))
|
||||
}
|
||||
|
||||
fn build_customer_info(
|
||||
payment_address: &types::PaymentAddress,
|
||||
email: &Option<Secret<String, Email>>,
|
||||
) -> Result<Customer, error_stack::Report<errors::ConnectorError>> {
|
||||
let (billing, address) =
|
||||
get_address(payment_address).ok_or(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
|
||||
let number_with_country_code = billing.phone.as_ref().and_then(|phone| {
|
||||
phone.number.as_ref().and_then(|number| {
|
||||
phone
|
||||
.country_code
|
||||
.as_ref()
|
||||
.map(|cc| Secret::new(format!("{}{}", cc, number.peek())))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Customer {
|
||||
billing_address: BillingAddress {
|
||||
..address.clone().into()
|
||||
},
|
||||
contact_details: Some(ContactDetails {
|
||||
mobile_phone_number: number_with_country_code,
|
||||
email_address: email.clone(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
impl From<api_models::AddressDetails> for BillingAddress {
|
||||
fn from(value: api_models::AddressDetails) -> Self {
|
||||
Self {
|
||||
city: value.city,
|
||||
country_code: value.country,
|
||||
state: value.state,
|
||||
zip: value.zip,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api_models::AddressDetails> for Shipping {
|
||||
fn from(value: api_models::AddressDetails) -> Self {
|
||||
Self {
|
||||
city: value.city,
|
||||
country_code: value.country,
|
||||
name: Some(Name {
|
||||
first_name: value.first_name,
|
||||
surname: value.last_name,
|
||||
..Default::default()
|
||||
}),
|
||||
state: value.state,
|
||||
zip: value.zip,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthType {
|
||||
pub api_key: String,
|
||||
pub api_secret: String,
|
||||
pub merchant_account_id: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for AuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
if let types::ConnectorAuthType::SignatureKey {
|
||||
api_key,
|
||||
key1,
|
||||
api_secret,
|
||||
} = auth_type
|
||||
{
|
||||
Ok(Self {
|
||||
api_key: api_key.to_string(),
|
||||
api_secret: api_secret.to_string(),
|
||||
merchant_account_id: key1.to_string(),
|
||||
})
|
||||
} else {
|
||||
Err(errors::ConnectorError::FailedToObtainAuthType)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaymentStatus {
|
||||
Captured,
|
||||
Paid,
|
||||
ChargebackNotification,
|
||||
Cancelled,
|
||||
Rejected,
|
||||
RejectedCapture,
|
||||
PendingApproval,
|
||||
CaptureRequested,
|
||||
#[default]
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl From<PaymentStatus> for enums::AttemptStatus {
|
||||
fn from(item: PaymentStatus) -> Self {
|
||||
match item {
|
||||
PaymentStatus::Captured
|
||||
| PaymentStatus::Paid
|
||||
| PaymentStatus::ChargebackNotification => Self::Charged,
|
||||
PaymentStatus::Cancelled => Self::Voided,
|
||||
PaymentStatus::Rejected | PaymentStatus::RejectedCapture => Self::Failure,
|
||||
PaymentStatus::CaptureRequested => Self::CaptureInitiated,
|
||||
PaymentStatus::PendingApproval => Self::Authorizing,
|
||||
_ => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct Payment {
|
||||
id: String,
|
||||
status: PaymentStatus,
|
||||
}
|
||||
|
||||
impl<F, T> TryFrom<types::ResponseRouterData<F, Payment, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, Payment, T, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(item.response.status.clone()),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||
redirection_data: None,
|
||||
redirect: false,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct PaymentResponse {
|
||||
payment: Payment,
|
||||
}
|
||||
|
||||
impl<F, T> TryFrom<types::ResponseRouterData<F, PaymentResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, PaymentResponse, T, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
status: enums::AttemptStatus::from(item.response.payment.status.clone()),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.payment.id),
|
||||
redirection_data: None,
|
||||
redirect: false,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct WorldlineRefundRequest {
|
||||
amount_of_money: AmountOfMoney,
|
||||
}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for WorldlineRefundRequest {
|
||||
type Error = error_stack::Report<errors::ParsingError>;
|
||||
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
amount_of_money: AmountOfMoney {
|
||||
amount: item.request.refund_amount,
|
||||
currency_code: item.request.currency.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default, Deserialize, Clone)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum RefundStatus {
|
||||
Cancelled,
|
||||
Rejected,
|
||||
Refunded,
|
||||
#[default]
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl From<RefundStatus> for enums::RefundStatus {
|
||||
fn from(item: RefundStatus) -> Self {
|
||||
match item {
|
||||
RefundStatus::Refunded => Self::Success,
|
||||
RefundStatus::Cancelled | RefundStatus::Rejected => Self::Failure,
|
||||
RefundStatus::Processing => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize)]
|
||||
pub struct RefundResponse {
|
||||
id: String,
|
||||
status: RefundStatus,
|
||||
}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
for types::RefundsRouterData<api::Execute>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ParsingError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let refund_status = enums::RefundStatus::from(item.response.status);
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: item.response.id.clone(),
|
||||
refund_status,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
|
||||
for types::RefundsRouterData<api::RSync>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ParsingError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let refund_status = enums::RefundStatus::from(item.response.status);
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: item.response.id.clone(),
|
||||
refund_status,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PaymentResponse> for enums::AttemptStatus {
|
||||
fn from(item: &PaymentResponse) -> Self {
|
||||
if item.payment.status == PaymentStatus::Cancelled {
|
||||
Self::Voided
|
||||
} else {
|
||||
Self::VoidFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Error {
|
||||
pub code: Option<String>,
|
||||
pub property_name: Option<String>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErrorResponse {
|
||||
pub error_id: Option<String>,
|
||||
pub errors: Vec<Error>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, Hash, PartialEq)]
|
||||
pub enum CardProduct {
|
||||
AmericanExpress,
|
||||
Master,
|
||||
Visa,
|
||||
Discover,
|
||||
}
|
||||
|
||||
impl CardProduct {
|
||||
fn product_id(&self) -> u16 {
|
||||
match *self {
|
||||
Self::AmericanExpress => 2,
|
||||
Self::Master => 3,
|
||||
Self::Visa => 1,
|
||||
Self::Discover => 128,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,6 +242,8 @@ pub enum ConnectorError {
|
||||
WebhookEventTypeNotFound,
|
||||
#[error("Incoming webhook event resource object not found")]
|
||||
WebhookResourceObjectNotFound,
|
||||
#[error("Invalid Date/time format")]
|
||||
InvalidDateFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@ -47,6 +47,7 @@ pub mod headers {
|
||||
pub const AUTHORIZATION: &str = "Authorization";
|
||||
pub const ACCEPT: &str = "Accept";
|
||||
pub const X_API_VERSION: &str = "X-ApiVersion";
|
||||
pub const DATE: &str = "Date";
|
||||
}
|
||||
|
||||
pub mod pii {
|
||||
|
||||
@ -152,6 +152,7 @@ impl ConnectorData {
|
||||
"payu" => Ok(Box::new(&connector::Payu)),
|
||||
"shift4" => Ok(Box::new(&connector::Shift4)),
|
||||
"stripe" => Ok(Box::new(&connector::Stripe)),
|
||||
"worldline" => Ok(Box::new(&connector::Worldline)),
|
||||
"worldpay" => Ok(Box::new(&connector::Worldpay)),
|
||||
_ => Err(report!(errors::ConnectorError::InvalidConnectorName)
|
||||
.attach_printable(format!("invalid connector name: {connector_name}")))
|
||||
|
||||
@ -11,6 +11,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub payu: Option<BodyKey>,
|
||||
pub shift4: Option<HeaderKey>,
|
||||
pub worldpay: Option<HeaderKey>,
|
||||
pub worldline: Option<SignatureKey>,
|
||||
}
|
||||
|
||||
impl ConnectorAuthentication {
|
||||
|
||||
@ -9,4 +9,5 @@ mod globalpay;
|
||||
mod payu;
|
||||
mod shift4;
|
||||
mod utils;
|
||||
mod worldline;
|
||||
mod worldpay;
|
||||
|
||||
@ -31,3 +31,7 @@ api_key = "MyApiKey"
|
||||
key1 = "MerchantID"
|
||||
api_secret = "MySecretKey"
|
||||
|
||||
[worldline]
|
||||
key1 = "Merchant Id"
|
||||
api_key = "API Key"
|
||||
api_secret = "API Secret Key"
|
||||
278
crates/router/tests/connectors/worldline.rs
Normal file
278
crates/router/tests/connectors/worldline.rs
Normal file
@ -0,0 +1,278 @@
|
||||
use api_models::payments::{Address, AddressDetails};
|
||||
use masking::Secret;
|
||||
use router::{
|
||||
connector::Worldline,
|
||||
types::{self, storage::enums, PaymentAddress},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
connector_auth::ConnectorAuthentication,
|
||||
utils::{self, ConnectorActions, PaymentInfo},
|
||||
};
|
||||
|
||||
struct WorldlineTest;
|
||||
|
||||
impl ConnectorActions for WorldlineTest {}
|
||||
impl utils::Connector for WorldlineTest {
|
||||
fn get_data(&self) -> types::api::ConnectorData {
|
||||
types::api::ConnectorData {
|
||||
connector: Box::new(&Worldline),
|
||||
connector_name: types::Connector::Worldline,
|
||||
get_token: types::api::GetToken::Connector,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auth_token(&self) -> types::ConnectorAuthType {
|
||||
types::ConnectorAuthType::from(
|
||||
ConnectorAuthentication::new()
|
||||
.worldline
|
||||
.expect("Missing connector authentication configuration"),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
String::from("worldline")
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldlineTest {
|
||||
fn get_payment_info() -> Option<PaymentInfo> {
|
||||
Some(PaymentInfo {
|
||||
address: Some(PaymentAddress {
|
||||
billing: Some(Address {
|
||||
address: Some(AddressDetails {
|
||||
country: Some("US".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
phone: None,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
auth_type: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_payment_authorize_data(
|
||||
card_number: &str,
|
||||
card_exp_month: &str,
|
||||
card_exp_year: &str,
|
||||
card_cvc: &str,
|
||||
capture_method: enums::CaptureMethod,
|
||||
) -> Option<types::PaymentsAuthorizeData> {
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
amount: 3500,
|
||||
currency: enums::Currency::USD,
|
||||
payment_method_data: types::api::PaymentMethod::Card(types::api::CCard {
|
||||
card_number: Secret::new(card_number.to_string()),
|
||||
card_exp_month: Secret::new(card_exp_month.to_string()),
|
||||
card_exp_year: Secret::new(card_exp_year.to_string()),
|
||||
card_holder_name: Secret::new("John Doe".to_string()),
|
||||
card_cvc: Secret::new(card_cvc.to_string()),
|
||||
}),
|
||||
confirm: true,
|
||||
statement_descriptor_suffix: None,
|
||||
setup_future_usage: None,
|
||||
mandate_id: None,
|
||||
off_session: None,
|
||||
setup_mandate_details: None,
|
||||
capture_method: Some(capture_method),
|
||||
browser_info: None,
|
||||
order_details: None,
|
||||
email: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_requires_manual_authorization() {
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"123",
|
||||
enums::CaptureMethod::Manual,
|
||||
);
|
||||
let response = WorldlineTest {}
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorizing);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_auto_authorize_and_request_capture() {
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"123",
|
||||
enums::CaptureMethod::Automatic,
|
||||
);
|
||||
let response = WorldlineTest {}
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::CaptureInitiated);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_invalid_cvc() {
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"",
|
||||
enums::CaptureMethod::Automatic,
|
||||
);
|
||||
let response = WorldlineTest {}
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"NULL VALUE NOT ALLOWED FOR cardPaymentMethodSpecificInput.card.cvv".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_sync_manual_auth_payment() {
|
||||
let connector = WorldlineTest {};
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"123",
|
||||
enums::CaptureMethod::Manual,
|
||||
);
|
||||
let response = connector
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorizing);
|
||||
let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default();
|
||||
let sync_response = connector
|
||||
.sync_payment(
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
connector_payment_id,
|
||||
),
|
||||
encoded_data: None,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(sync_response.status, enums::AttemptStatus::Authorizing);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_sync_auto_auth_payment() {
|
||||
let connector = WorldlineTest {};
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"123",
|
||||
enums::CaptureMethod::Automatic,
|
||||
);
|
||||
let response = connector
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::CaptureInitiated);
|
||||
let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default();
|
||||
let sync_response = connector
|
||||
.sync_payment(
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
connector_payment_id,
|
||||
),
|
||||
encoded_data: None,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(sync_response.status, enums::AttemptStatus::CaptureInitiated);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_fail_capture_payment() {
|
||||
let capture_response = WorldlineTest {}
|
||||
.capture_payment("123456789".to_string(), None, None)
|
||||
.await;
|
||||
assert_eq!(
|
||||
capture_response.response.unwrap_err().message,
|
||||
"Something went wrong.".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_cancel_unauthorized_payment() {
|
||||
let connector = WorldlineTest {};
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"123",
|
||||
enums::CaptureMethod::Manual,
|
||||
);
|
||||
let response = connector
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorizing);
|
||||
let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default();
|
||||
let cancel_response = connector
|
||||
.void_payment(connector_payment_id, None, None)
|
||||
.await;
|
||||
assert_eq!(cancel_response.status, enums::AttemptStatus::Voided);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_cancel_uncaptured_payment() {
|
||||
let connector = WorldlineTest {};
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"123",
|
||||
enums::CaptureMethod::Automatic,
|
||||
);
|
||||
let response = connector
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::CaptureInitiated);
|
||||
let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default();
|
||||
let cancel_response = connector
|
||||
.void_payment(connector_payment_id, None, None)
|
||||
.await;
|
||||
assert_eq!(cancel_response.status, enums::AttemptStatus::Voided);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_fail_cancel_with_invalid_payment_id() {
|
||||
let response = WorldlineTest {}
|
||||
.void_payment("123456789".to_string(), None, None)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"UNKNOWN_PAYMENT_ID".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn should_fail_refund_with_invalid_payment_status() {
|
||||
let connector = WorldlineTest {};
|
||||
let authorize_data = WorldlineTest::get_payment_authorize_data(
|
||||
"4012000033330026",
|
||||
"10",
|
||||
"2025",
|
||||
"123",
|
||||
enums::CaptureMethod::Manual,
|
||||
);
|
||||
let response = connector
|
||||
.make_payment(authorize_data, WorldlineTest::get_payment_info())
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorizing);
|
||||
let connector_payment_id = utils::get_connector_transaction_id(response).unwrap_or_default();
|
||||
let refund_response = connector
|
||||
.refund_payment(connector_payment_id, None, None)
|
||||
.await;
|
||||
assert_eq!(
|
||||
refund_response.response.unwrap_err().message,
|
||||
"ORDER WITHOUT REFUNDABLE PAYMENTS".to_string(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user