feature(connector): add support for worldline connector (#374)

This commit is contained in:
Arjun Karthik
2023-01-16 18:23:44 +05:30
committed by GitHub
parent d01634891e
commit a16fc653cf
15 changed files with 1390 additions and 1 deletions

1
Cargo.lock generated
View File

@ -3015,6 +3015,7 @@ dependencies = [
"once_cell",
"rand 0.8.5",
"redis_interface",
"regex",
"reqwest",
"ring",
"router_derive",

View File

@ -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"

View File

@ -509,6 +509,7 @@ pub enum Connector {
Payu,
Shift4,
Stripe,
Worldline,
Worldpay,
}

View File

@ -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"] }

View File

@ -134,6 +134,7 @@ pub struct Connectors {
pub shift4: ConnectorParams,
pub stripe: ConnectorParams,
pub supported: SupportedConnectors,
pub worldline: ConnectorParams,
pub worldpay: ConnectorParams,
}

View File

@ -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,
};

View 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 {}

View 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,
}
}
}

View File

@ -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)]

View File

@ -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 {

View File

@ -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}")))

View File

@ -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 {

View File

@ -9,4 +9,5 @@ mod globalpay;
mod payu;
mod shift4;
mod utils;
mod worldline;
mod worldpay;

View File

@ -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"

View 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(),
);
}