mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feature(connector): add support for worldpay connector (#272)
This commit is contained in:
@ -87,6 +87,8 @@ rand = "0.8.5"
|
||||
time = { version = "0.3.17", features = ["macros"] }
|
||||
tokio = "1.23.0"
|
||||
toml = "0.5.9"
|
||||
serial_test = "0.10.0"
|
||||
wiremock = "0.5"
|
||||
|
||||
[[bin]]
|
||||
name = "router"
|
||||
|
||||
@ -117,6 +117,7 @@ pub struct Connectors {
|
||||
pub shift4: ConnectorParams,
|
||||
pub stripe: ConnectorParams,
|
||||
pub supported: SupportedConnectors,
|
||||
pub worldpay: ConnectorParams,
|
||||
pub applepay: ConnectorParams,
|
||||
}
|
||||
|
||||
|
||||
@ -6,12 +6,12 @@ pub mod braintree;
|
||||
pub mod checkout;
|
||||
pub mod cybersource;
|
||||
pub mod klarna;
|
||||
pub mod stripe;
|
||||
|
||||
pub mod shift4;
|
||||
pub mod stripe;
|
||||
pub mod worldpay;
|
||||
|
||||
pub use self::{
|
||||
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
|
||||
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, klarna::Klarna,
|
||||
shift4::Shift4, stripe::Stripe,
|
||||
shift4::Shift4, stripe::Stripe, worldpay::Worldpay,
|
||||
};
|
||||
|
||||
@ -439,6 +439,21 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
|
||||
Ok(format!("{}refunds", self.base_url(connectors),))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::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)?)
|
||||
.body(types::RefundSyncType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundSyncRouterData,
|
||||
|
||||
613
crates/router/src/connector/worldpay.rs
Normal file
613
crates/router/src/connector/worldpay.rs
Normal file
@ -0,0 +1,613 @@
|
||||
mod requests;
|
||||
mod response;
|
||||
mod transformers;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use bytes::Bytes;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use storage_models::enums;
|
||||
use transformers as worldpay;
|
||||
|
||||
use self::{requests::*, response::*};
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::{
|
||||
errors::{self, CustomResult},
|
||||
payments,
|
||||
},
|
||||
headers, logger,
|
||||
services::{self, ConnectorIntegration},
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCommon, ConnectorCommonExt},
|
||||
ErrorResponse, Response,
|
||||
},
|
||||
utils::{self, BytesExt},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Worldpay;
|
||||
|
||||
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Worldpay
|
||||
where
|
||||
Self: ConnectorIntegration<Flow, Request, Response>,
|
||||
{
|
||||
fn build_headers(
|
||||
&self,
|
||||
req: &types::RouterData<Flow, Request, Response>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let mut headers = vec![(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
self.get_content_type().to_string(),
|
||||
)];
|
||||
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
|
||||
headers.append(&mut api_key);
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorCommon for Worldpay {
|
||||
fn id(&self) -> &'static str {
|
||||
"worldpay"
|
||||
}
|
||||
|
||||
fn common_get_content_type(&self) -> &'static str {
|
||||
"application/vnd.worldpay.payments-v6+json"
|
||||
}
|
||||
|
||||
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
|
||||
connectors.worldpay.base_url.as_ref()
|
||||
}
|
||||
|
||||
fn get_auth_header(
|
||||
&self,
|
||||
auth_type: &types::ConnectorAuthType,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let auth: worldpay::WorldpayAuthType = auth_type
|
||||
.try_into()
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)])
|
||||
}
|
||||
|
||||
fn build_error_response(
|
||||
&self,
|
||||
res: Bytes,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
let response: WorldpayErrorResponse = res
|
||||
.parse_struct("WorldpayErrorResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
Ok(ErrorResponse {
|
||||
code: response.error_name,
|
||||
message: response.message,
|
||||
reason: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Payment for Worldpay {}
|
||||
|
||||
impl api::PreVerify for Worldpay {}
|
||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
}
|
||||
|
||||
impl api::PaymentVoid for Worldpay {}
|
||||
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCancelRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::PaymentsCancelRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let connector_payment_id = req.request.connector_transaction_id.clone();
|
||||
Ok(format!(
|
||||
"{}payments/settlements/{}",
|
||||
self.base_url(connectors),
|
||||
connector_payment_id
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsCancelRouterData,
|
||||
connectors: &settings::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>
|
||||
where
|
||||
api::Void: Clone,
|
||||
types::PaymentsCancelData: Clone,
|
||||
types::PaymentsResponseData: Clone,
|
||||
{
|
||||
match res.status_code {
|
||||
202 => {
|
||||
let response: WorldpayPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Worldpay PaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
Ok(types::PaymentsCancelRouterData {
|
||||
status: enums::AttemptStatus::Voided,
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::try_from(response.links)?,
|
||||
redirection_data: None,
|
||||
redirect: false,
|
||||
mandate_reference: None,
|
||||
}),
|
||||
..data.clone()
|
||||
})
|
||||
}
|
||||
_ => Err(errors::ConnectorError::ResponseHandlingFailed)?,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Bytes,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentSync for Worldpay {}
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let connector_payment_id = req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
Ok(format!(
|
||||
"{}payments/events/{}",
|
||||
self.base_url(connectors),
|
||||
connector_payment_id
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::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)?)
|
||||
.body(types::PaymentsSyncType::get_request_body(self, req)?)
|
||||
.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> {
|
||||
let response: WorldpayEventResponse =
|
||||
res.response
|
||||
.parse_struct("Worldpay EventResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
Ok(types::PaymentsSyncRouterData {
|
||||
status: enums::AttemptStatus::from(response.last_event),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: data.request.connector_transaction_id.clone(),
|
||||
redirection_data: None,
|
||||
redirect: false,
|
||||
mandate_reference: None,
|
||||
}),
|
||||
..data.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentCapture for Worldpay {}
|
||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
|
||||
.headers(types::PaymentsCaptureType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCaptureRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||
logger::debug!(worldpaypayments_capture_response=?res);
|
||||
match res.status_code {
|
||||
202 => {
|
||||
let response: WorldpayPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Worldpay PaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
Ok(types::PaymentsCaptureRouterData {
|
||||
status: enums::AttemptStatus::Charged,
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::try_from(response.links)?,
|
||||
redirection_data: None,
|
||||
redirect: false,
|
||||
mandate_reference: None,
|
||||
}),
|
||||
..data.clone()
|
||||
})
|
||||
}
|
||||
_ => Err(errors::ConnectorError::ResponseHandlingFailed)?,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let connector_payment_id = req.request.connector_transaction_id.clone();
|
||||
Ok(format!(
|
||||
"{}payments/settlements/{}",
|
||||
self.base_url(connectors),
|
||||
connector_payment_id
|
||||
))
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Bytes,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::PaymentSession for Worldpay {}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
}
|
||||
|
||||
impl api::PaymentAuthorize for Worldpay {}
|
||||
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!(
|
||||
"{}payments/authorizations",
|
||||
self.base_url(connectors)
|
||||
))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let worldpay_req = utils::Encode::<WorldpayPaymentsRequest>::convert_and_encode(req)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(worldpay_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::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: WorldpayPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Worldpay PaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
logger::debug!(worldpaypayments_create_response=?response);
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Bytes,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl api::Refund for Worldpay {}
|
||||
impl api::RefundExecute for Worldpay {}
|
||||
impl api::RefundSync for Worldpay {}
|
||||
|
||||
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
|
||||
for Worldpay
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::RefundExecuteRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req = utils::Encode::<WorldpayRefundRequest>::convert_and_encode(req)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(req))
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let connector_payment_id = req.request.connector_transaction_id.clone();
|
||||
Ok(format!(
|
||||
"{}payments/settlements/refunds/partials/{}",
|
||||
self.base_url(connectors),
|
||||
connector_payment_id
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
connectors: &settings::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::worldpay", response=?res);
|
||||
match res.status_code {
|
||||
202 => {
|
||||
let response: WorldpayPaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("Worldpay PaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
Ok(types::RefundExecuteRouterData {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: ResponseIdStr::try_from(response.links)?.id,
|
||||
refund_status: enums::RefundStatus::Success,
|
||||
}),
|
||||
..data.clone()
|
||||
})
|
||||
}
|
||||
_ => Err(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 Worldpay {
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
self.build_headers(req, connectors)
|
||||
}
|
||||
|
||||
fn get_content_type(&self) -> &'static str {
|
||||
self.common_get_content_type()
|
||||
}
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
Ok(format!(
|
||||
"{}payments/events/{}",
|
||||
self.base_url(connectors),
|
||||
req.request.connector_transaction_id
|
||||
))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::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)?)
|
||||
.body(types::RefundSyncType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||
let response: WorldpayEventResponse =
|
||||
res.response
|
||||
.parse_struct("Worldpay EventResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
Ok(types::RefundSyncRouterData {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: data.request.refund_id.clone(),
|
||||
refund_status: enums::RefundStatus::from(response.last_event),
|
||||
}),
|
||||
..data.clone()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Bytes,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl api::IncomingWebhook for Worldpay {
|
||||
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 Worldpay {
|
||||
fn get_flow_type(
|
||||
&self,
|
||||
_query_params: &str,
|
||||
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
|
||||
Ok(payments::CallConnectorAction::Trigger)
|
||||
}
|
||||
}
|
||||
225
crates/router/src/connector/worldpay/requests.rs
Normal file
225
crates/router/src/connector/worldpay/requests.rs
Normal file
@ -0,0 +1,225 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BillingAddress {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub city: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address2: Option<String>,
|
||||
pub postal_code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address3: Option<String>,
|
||||
pub country_code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub address1: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorldpayPaymentsRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub channel: Option<Channel>,
|
||||
pub instruction: Instruction,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub customer: Option<Customer>,
|
||||
pub merchant: Merchant,
|
||||
pub transaction_reference: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Channel {
|
||||
#[default]
|
||||
Moto,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Customer {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub risk_profile: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub authentication: Option<CustomerAuthentication>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CustomerAuthentication {
|
||||
ThreeDS(ThreeDS),
|
||||
Token(NetworkToken),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ThreeDS {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub authentication_value: Option<String>,
|
||||
pub version: ThreeDSVersion,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transaction_id: Option<String>,
|
||||
pub eci: String,
|
||||
#[serde(rename = "type")]
|
||||
pub auth_type: CustomerAuthType,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum ThreeDSVersion {
|
||||
#[default]
|
||||
#[serde(rename = "1")]
|
||||
One,
|
||||
#[serde(rename = "2")]
|
||||
Two,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum CustomerAuthType {
|
||||
#[serde(rename = "3DS")]
|
||||
#[default]
|
||||
Variant3Ds,
|
||||
#[serde(rename = "card/networkToken")]
|
||||
NetworkToken,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NetworkToken {
|
||||
#[serde(rename = "type")]
|
||||
pub auth_type: CustomerAuthType,
|
||||
pub authentication_value: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub eci: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Instruction {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub debt_repayment: Option<bool>,
|
||||
pub value: PaymentValue,
|
||||
pub narrative: InstructionNarrative,
|
||||
pub payment_instrument: PaymentInstrument,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstructionNarrative {
|
||||
pub line1: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub line2: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PaymentInstrument {
|
||||
Card(CardPayment),
|
||||
CardToken(CardToken),
|
||||
Googlepay(WalletPayment),
|
||||
Applepay(WalletPayment),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Eq, Default, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum PaymentType {
|
||||
#[default]
|
||||
#[serde(rename = "card/plain")]
|
||||
Card,
|
||||
#[serde(rename = "card/token")]
|
||||
CardToken,
|
||||
#[serde(rename = "card/wallet+googlepay")]
|
||||
Googlepay,
|
||||
#[serde(rename = "card/wallet+applepay")]
|
||||
Applepay,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CardPayment {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub billing_address: Option<BillingAddress>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub card_holder_name: Option<String>,
|
||||
pub card_expiry_date: CardExpiryDate,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cvc: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub payment_type: PaymentType,
|
||||
pub card_number: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CardToken {
|
||||
#[serde(rename = "type")]
|
||||
pub payment_type: PaymentType,
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WalletPayment {
|
||||
#[serde(rename = "type")]
|
||||
pub payment_type: PaymentType,
|
||||
pub wallet_token: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub billing_address: Option<BillingAddress>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct CardExpiryDate {
|
||||
pub month: u8,
|
||||
pub year: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PaymentValue {
|
||||
pub amount: i64,
|
||||
pub currency: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Merchant {
|
||||
pub entity: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mcc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub payment_facilitator: Option<PaymentFacilitator>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaymentFacilitator {
|
||||
pub pf_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iso_id: Option<String>,
|
||||
pub sub_merchant: SubMerchant,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubMerchant {
|
||||
pub city: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<String>,
|
||||
pub postal_code: String,
|
||||
pub merchant_id: String,
|
||||
pub country_code: String,
|
||||
pub street: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tax_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct WorldpayRefundRequest {
|
||||
pub value: PaymentValue,
|
||||
pub reference: String,
|
||||
}
|
||||
306
crates/router/src/connector/worldpay/response.rs
Normal file
306
crates/router/src/connector/worldpay/response.rs
Normal file
@ -0,0 +1,306 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{core::errors, types};
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorldpayPaymentsResponse {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exemption: Option<Exemption>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub issuer: Option<Issuer>,
|
||||
pub outcome: Option<Outcome>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub payment_instrument: Option<PaymentsResPaymentInstrument>,
|
||||
/// Any risk factors which have been identified for the authorization. This section will not appear if no risks are identified.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub risk_factors: Option<Vec<RiskFactorsInner>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scheme: Option<PaymentsResponseScheme>,
|
||||
#[serde(rename = "_links", skip_serializing_if = "Option::is_none")]
|
||||
pub links: Option<PaymentLinks>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Outcome {
|
||||
Authorized,
|
||||
Refused,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorldpayEventResponse {
|
||||
pub last_event: EventType,
|
||||
#[serde(rename = "_links", skip_serializing_if = "Option::is_none")]
|
||||
pub links: Option<EventLinks>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EventType {
|
||||
Authorized,
|
||||
Cancelled,
|
||||
Charged,
|
||||
SentForRefund,
|
||||
RefundFailed,
|
||||
Refused,
|
||||
Refunded,
|
||||
Error,
|
||||
CaptureFailed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Exemption {
|
||||
pub result: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PaymentLinks {
|
||||
#[serde(rename = "payments:events", skip_serializing_if = "Option::is_none")]
|
||||
pub events: Option<PaymentLink>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct EventLinks {
|
||||
#[serde(rename = "payments:events", skip_serializing_if = "Option::is_none")]
|
||||
pub events: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PaymentLink {
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
fn get_resource_id<T, F>(
|
||||
links: Option<PaymentLinks>,
|
||||
transform_fn: F,
|
||||
) -> Result<T, error_stack::Report<errors::ConnectorError>>
|
||||
where
|
||||
F: Fn(String) -> T,
|
||||
{
|
||||
let reference_id = links
|
||||
.and_then(|l| l.events)
|
||||
.and_then(|e| e.href.rsplit_once('/').map(|h| h.1.to_string()))
|
||||
.map(transform_fn);
|
||||
reference_id.ok_or_else(|| {
|
||||
errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "links.events".to_string(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ResponseIdStr {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl TryFrom<Option<PaymentLinks>> for ResponseIdStr {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(links: Option<PaymentLinks>) -> Result<Self, Self::Error> {
|
||||
get_resource_id(links, |id| Self { id })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Option<PaymentLinks>> for types::ResponseId {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(links: Option<PaymentLinks>) -> Result<Self, Self::Error> {
|
||||
get_resource_id(links, Self::ConnectorTransactionId)
|
||||
}
|
||||
}
|
||||
|
||||
impl Exemption {
|
||||
pub fn new(result: String, reason: String) -> Self {
|
||||
Self { result, reason }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Issuer {
|
||||
pub authorization_code: String,
|
||||
}
|
||||
|
||||
impl Issuer {
|
||||
pub fn new(authorization_code: String) -> Self {
|
||||
Self { authorization_code }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PaymentsResPaymentInstrument {
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub risk_type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub card: Option<PaymentInstrumentCard>,
|
||||
}
|
||||
|
||||
impl PaymentsResPaymentInstrument {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
risk_type: None,
|
||||
card: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaymentInstrumentCard {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub number: Option<PaymentInstrumentCardNumber>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub issuer: Option<PaymentInstrumentCardIssuer>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub payment_account_reference: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub country_code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub funding_type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub brand: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expiry_date: Option<PaymentInstrumentCardExpiryDate>,
|
||||
}
|
||||
|
||||
impl PaymentInstrumentCard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
number: None,
|
||||
issuer: None,
|
||||
payment_account_reference: None,
|
||||
country_code: None,
|
||||
funding_type: None,
|
||||
brand: None,
|
||||
expiry_date: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaymentInstrumentCardExpiryDate {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub month: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub year: Option<i32>,
|
||||
}
|
||||
|
||||
impl PaymentInstrumentCardExpiryDate {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
month: None,
|
||||
year: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaymentInstrumentCardIssuer {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl PaymentInstrumentCardIssuer {
|
||||
pub fn new() -> Self {
|
||||
Self { name: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaymentInstrumentCardNumber {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bin: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last4_digits: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dpan: Option<String>,
|
||||
}
|
||||
|
||||
impl PaymentInstrumentCardNumber {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bin: None,
|
||||
last4_digits: None,
|
||||
dpan: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RiskFactorsInner {
|
||||
#[serde(rename = "type")]
|
||||
pub risk_type: RiskType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail: Option<Detail>,
|
||||
pub risk: Risk,
|
||||
}
|
||||
|
||||
impl RiskFactorsInner {
|
||||
pub fn new(risk_type: RiskType, risk: Risk) -> Self {
|
||||
Self {
|
||||
risk_type,
|
||||
detail: None,
|
||||
risk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum RiskType {
|
||||
#[default]
|
||||
Avs,
|
||||
Cvc,
|
||||
RiskProfile,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Detail {
|
||||
#[default]
|
||||
Address,
|
||||
Postcode,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum Risk {
|
||||
#[default]
|
||||
#[serde(rename = "not_checked")]
|
||||
NotChecked,
|
||||
#[serde(rename = "not_matched")]
|
||||
NotMatched,
|
||||
#[serde(rename = "not_supplied")]
|
||||
NotSupplied,
|
||||
#[serde(rename = "verificationFailed")]
|
||||
VerificationFailed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PaymentsResponseScheme {
|
||||
pub reference: String,
|
||||
}
|
||||
|
||||
impl PaymentsResponseScheme {
|
||||
pub fn new(reference: String) -> Self {
|
||||
Self { reference }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorldpayErrorResponse {
|
||||
pub error_name: String,
|
||||
pub message: String,
|
||||
}
|
||||
179
crates/router/src/connector/worldpay/transformers.rs
Normal file
179
crates/router/src/connector/worldpay/transformers.rs
Normal file
@ -0,0 +1,179 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use common_utils::errors::CustomResult;
|
||||
use masking::PeekInterface;
|
||||
use storage_models::enums;
|
||||
|
||||
use super::{requests::*, response::*};
|
||||
use crate::{
|
||||
core::errors,
|
||||
types::{self, api},
|
||||
};
|
||||
|
||||
fn parse_int<T: FromStr>(
|
||||
val: masking::Secret<String, masking::WithType>,
|
||||
) -> CustomResult<T, errors::ConnectorError>
|
||||
where
|
||||
<T as FromStr>::Err: Sync,
|
||||
{
|
||||
let res = val.peek().parse::<T>();
|
||||
if let Ok(val) = res {
|
||||
Ok(val)
|
||||
} else {
|
||||
Err(errors::ConnectorError::RequestEncodingFailed)?
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_payment_instrument(
|
||||
payment_method: api::PaymentMethod,
|
||||
) -> CustomResult<PaymentInstrument, errors::ConnectorError> {
|
||||
match payment_method {
|
||||
api::PaymentMethod::Card(card) => Ok(PaymentInstrument::Card(CardPayment {
|
||||
card_expiry_date: CardExpiryDate {
|
||||
month: parse_int::<u8>(card.card_exp_month)?,
|
||||
year: parse_int::<u16>(card.card_exp_year)?,
|
||||
},
|
||||
card_number: card.card_number.peek().to_string(),
|
||||
..CardPayment::default()
|
||||
})),
|
||||
api::PaymentMethod::Wallet(wallet) => match wallet.issuer_name {
|
||||
api_models::enums::WalletIssuer::ApplePay => {
|
||||
Ok(PaymentInstrument::Applepay(WalletPayment {
|
||||
payment_type: PaymentType::Applepay,
|
||||
wallet_token: wallet.token,
|
||||
..WalletPayment::default()
|
||||
}))
|
||||
}
|
||||
api_models::enums::WalletIssuer::GooglePay => {
|
||||
Ok(PaymentInstrument::Googlepay(WalletPayment {
|
||||
payment_type: PaymentType::Googlepay,
|
||||
wallet_token: wallet.token,
|
||||
..WalletPayment::default()
|
||||
}))
|
||||
}
|
||||
_ => Err(errors::ConnectorError::NotImplemented("Wallet Type".to_string()).into()),
|
||||
},
|
||||
_ => {
|
||||
Err(errors::ConnectorError::NotImplemented("Current Payment Method".to_string()).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for WorldpayPaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
instruction: Instruction {
|
||||
value: PaymentValue {
|
||||
amount: item.request.amount,
|
||||
currency: item.request.currency.to_string(),
|
||||
},
|
||||
narrative: InstructionNarrative {
|
||||
line1: item.merchant_id.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
payment_instrument: fetch_payment_instrument(
|
||||
item.request.payment_method_data.clone(),
|
||||
)?,
|
||||
debt_repayment: None,
|
||||
},
|
||||
merchant: Merchant {
|
||||
entity: item.payment_id.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
transaction_reference: item.attempt_id.clone().ok_or(
|
||||
errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "attempt_id".to_string(),
|
||||
},
|
||||
)?,
|
||||
channel: None,
|
||||
customer: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WorldpayAuthType {
|
||||
pub(super) api_key: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for WorldpayAuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
match auth_type {
|
||||
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self {
|
||||
api_key: api_key.to_string(),
|
||||
}),
|
||||
_ => Err(errors::ConnectorError::FailedToObtainAuthType)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Outcome> for enums::AttemptStatus {
|
||||
fn from(item: Outcome) -> Self {
|
||||
match item {
|
||||
Outcome::Authorized => Self::Authorized,
|
||||
Outcome::Refused => Self::Failure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventType> for enums::AttemptStatus {
|
||||
fn from(value: EventType) -> Self {
|
||||
match value {
|
||||
EventType::Authorized => Self::Authorized,
|
||||
EventType::CaptureFailed => Self::CaptureFailed,
|
||||
EventType::Refused => Self::Failure,
|
||||
EventType::Charged => Self::Charged,
|
||||
_ => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventType> for enums::RefundStatus {
|
||||
fn from(value: EventType) -> Self {
|
||||
match value {
|
||||
EventType::Refunded => Self::Success,
|
||||
EventType::RefundFailed => Self::Failure,
|
||||
_ => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<types::PaymentsResponseRouterData<WorldpayPaymentsResponse>>
|
||||
for types::PaymentsAuthorizeRouterData
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::PaymentsResponseRouterData<WorldpayPaymentsResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
status: match item.response.outcome {
|
||||
Some(outcome) => enums::AttemptStatus::from(outcome),
|
||||
None => Err(errors::ConnectorError::MissingRequiredField {
|
||||
field_name: "outcome".to_string(),
|
||||
})?,
|
||||
},
|
||||
description: item.response.description,
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::try_from(item.response.links)?,
|
||||
redirection_data: None,
|
||||
redirect: false,
|
||||
mandate_reference: None,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for WorldpayRefundRequest {
|
||||
type Error = error_stack::Report<errors::ParsingError>;
|
||||
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
reference: item.request.connector_transaction_id.clone(),
|
||||
value: PaymentValue {
|
||||
amount: item.request.amount,
|
||||
currency: item.request.currency.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -241,8 +241,8 @@ pub enum ApiClientError {
|
||||
|
||||
#[error("URL encoding of request payload failed")]
|
||||
UrlEncodingFailed,
|
||||
#[error("Failed to send request to connector")]
|
||||
RequestNotSent,
|
||||
#[error("Failed to send request to connector {0}")]
|
||||
RequestNotSent(String),
|
||||
#[error("Failed to decode response")]
|
||||
ResponseDecodingFailed,
|
||||
|
||||
|
||||
@ -79,6 +79,7 @@ where
|
||||
merchant_id: merchant_account.merchant_id.clone(),
|
||||
connector: merchant_connector_account.connector_name,
|
||||
payment_id: payment_data.payment_attempt.payment_id.clone(),
|
||||
attempt_id: Some(payment_data.payment_attempt.attempt_id.clone()),
|
||||
status: payment_data.payment_attempt.status,
|
||||
payment_method,
|
||||
connector_auth_type: auth_type,
|
||||
|
||||
@ -66,6 +66,7 @@ pub async fn construct_refund_router_data<'a, F>(
|
||||
merchant_id: merchant_account.merchant_id.clone(),
|
||||
connector: merchant_connector_account.connector_name,
|
||||
payment_id: payment_attempt.payment_id.clone(),
|
||||
attempt_id: Some(payment_attempt.attempt_id.clone()),
|
||||
status,
|
||||
payment_method: payment_method_type,
|
||||
connector_auth_type: auth_type,
|
||||
|
||||
@ -245,7 +245,7 @@ async fn send_request(
|
||||
}
|
||||
.map_err(|error| match error {
|
||||
error if error.is_timeout() => errors::ApiClientError::RequestTimeoutReceived,
|
||||
_ => errors::ApiClientError::RequestNotSent,
|
||||
_ => errors::ApiClientError::RequestNotSent(error.to_string()),
|
||||
})
|
||||
.into_report()
|
||||
.attach_printable("Unable to send request to connector")
|
||||
|
||||
@ -69,6 +69,7 @@ pub struct RouterData<Flow, Request, Response> {
|
||||
pub merchant_id: String,
|
||||
pub connector: String,
|
||||
pub payment_id: String,
|
||||
pub attempt_id: Option<String>,
|
||||
pub status: storage_enums::AttemptStatus,
|
||||
pub payment_method: storage_enums::PaymentMethodType,
|
||||
pub connector_auth_type: ConnectorAuthType,
|
||||
|
||||
@ -149,6 +149,7 @@ impl ConnectorData {
|
||||
"applepay" => Ok(Box::new(&connector::Applepay)),
|
||||
"cybersource" => Ok(Box::new(&connector::Cybersource)),
|
||||
"shift4" => Ok(Box::new(&connector::Shift4)),
|
||||
"worldpay" => Ok(Box::new(&connector::Worldpay)),
|
||||
_ => Err(report!(errors::UnexpectedError)
|
||||
.attach_printable(format!("invalid connector name: {connector_name}")))
|
||||
.change_context(errors::ConnectorError::InvalidConnectorName)
|
||||
|
||||
@ -22,6 +22,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
|
||||
merchant_id: String::from("aci"),
|
||||
connector: "aci".to_string(),
|
||||
payment_id: uuid::Uuid::new_v4().to_string(),
|
||||
attempt_id: None,
|
||||
status: enums::AttemptStatus::default(),
|
||||
auth_type: enums::AuthenticationType::NoThreeDs,
|
||||
payment_method: enums::PaymentMethodType::Card,
|
||||
@ -68,6 +69,7 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
|
||||
merchant_id: String::from("aci"),
|
||||
connector: "aci".to_string(),
|
||||
payment_id: uuid::Uuid::new_v4().to_string(),
|
||||
attempt_id: None,
|
||||
status: enums::AttemptStatus::default(),
|
||||
router_return_url: None,
|
||||
payment_method: enums::PaymentMethodType::Card,
|
||||
|
||||
@ -22,6 +22,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
|
||||
merchant_id: String::from("authorizedotnet"),
|
||||
connector: "authorizedotnet".to_string(),
|
||||
payment_id: uuid::Uuid::new_v4().to_string(),
|
||||
attempt_id: None,
|
||||
status: enums::AttemptStatus::default(),
|
||||
router_return_url: None,
|
||||
payment_method: enums::PaymentMethodType::Card,
|
||||
@ -69,6 +70,7 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
|
||||
merchant_id: String::from("authorizedotnet"),
|
||||
connector: "authorizedotnet".to_string(),
|
||||
payment_id: uuid::Uuid::new_v4().to_string(),
|
||||
attempt_id: None,
|
||||
status: enums::AttemptStatus::default(),
|
||||
router_return_url: None,
|
||||
auth_type: enums::AuthenticationType::NoThreeDs,
|
||||
|
||||
@ -19,6 +19,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
|
||||
merchant_id: "checkout".to_string(),
|
||||
connector: "checkout".to_string(),
|
||||
payment_id: uuid::Uuid::new_v4().to_string(),
|
||||
attempt_id: None,
|
||||
status: enums::AttemptStatus::default(),
|
||||
router_return_url: None,
|
||||
auth_type: enums::AuthenticationType::NoThreeDs,
|
||||
@ -66,6 +67,7 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
|
||||
merchant_id: "checkout".to_string(),
|
||||
connector: "checkout".to_string(),
|
||||
payment_id: uuid::Uuid::new_v4().to_string(),
|
||||
attempt_id: None,
|
||||
status: enums::AttemptStatus::default(),
|
||||
router_return_url: None,
|
||||
payment_method: enums::PaymentMethodType::Card,
|
||||
|
||||
@ -7,6 +7,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub authorizedotnet: Option<BodyKey>,
|
||||
pub checkout: Option<BodyKey>,
|
||||
pub shift4: Option<HeaderKey>,
|
||||
pub worldpay: Option<HeaderKey>,
|
||||
}
|
||||
|
||||
impl ConnectorAuthentication {
|
||||
|
||||
@ -6,3 +6,4 @@ mod checkout;
|
||||
mod connector_auth;
|
||||
mod shift4;
|
||||
mod utils;
|
||||
mod worldpay;
|
||||
|
||||
@ -14,4 +14,7 @@ api_key = "Bearer MyApiKey"
|
||||
key1 = "MyProcessingChannelId"
|
||||
|
||||
[shift4]
|
||||
api_key = "Bearer MyApiKey"
|
||||
|
||||
[worldpay]
|
||||
api_key = "Bearer MyApiKey"
|
||||
@ -8,6 +8,7 @@ use router::{
|
||||
routes, services,
|
||||
types::{self, api, storage::enums, PaymentAddress},
|
||||
};
|
||||
use wiremock::{Mock, MockServer};
|
||||
|
||||
pub trait Connector {
|
||||
fn get_data(&self) -> types::api::ConnectorData;
|
||||
@ -25,6 +26,7 @@ pub trait ConnectorActions: Connector {
|
||||
let request = generate_data(
|
||||
self.get_name(),
|
||||
self.get_auth_token(),
|
||||
enums::AuthenticationType::NoThreeDs,
|
||||
payment_data.unwrap_or_else(|| types::PaymentsAuthorizeData {
|
||||
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
|
||||
..PaymentAuthorizeType::default().0
|
||||
@ -40,10 +42,26 @@ pub trait ConnectorActions: Connector {
|
||||
let request = generate_data(
|
||||
self.get_name(),
|
||||
self.get_auth_token(),
|
||||
enums::AuthenticationType::NoThreeDs,
|
||||
payment_data.unwrap_or_else(|| PaymentAuthorizeType::default().0),
|
||||
);
|
||||
call_connector(request, integration).await
|
||||
}
|
||||
|
||||
async fn sync_payment(
|
||||
&self,
|
||||
payment_data: Option<types::PaymentsSyncData>,
|
||||
) -> types::PaymentsSyncRouterData {
|
||||
let integration = self.get_data().connector.get_connector_integration();
|
||||
let request = generate_data(
|
||||
self.get_name(),
|
||||
self.get_auth_token(),
|
||||
enums::AuthenticationType::NoThreeDs,
|
||||
payment_data.unwrap_or_else(|| PaymentSyncType::default().0),
|
||||
);
|
||||
call_connector(request, integration).await
|
||||
}
|
||||
|
||||
async fn capture_payment(
|
||||
&self,
|
||||
transaction_id: String,
|
||||
@ -53,6 +71,7 @@ pub trait ConnectorActions: Connector {
|
||||
let request = generate_data(
|
||||
self.get_name(),
|
||||
self.get_auth_token(),
|
||||
enums::AuthenticationType::NoThreeDs,
|
||||
payment_data.unwrap_or(types::PaymentsCaptureData {
|
||||
amount_to_capture: Some(100),
|
||||
connector_transaction_id: transaction_id,
|
||||
@ -62,6 +81,25 @@ pub trait ConnectorActions: Connector {
|
||||
);
|
||||
call_connector(request, integration).await
|
||||
}
|
||||
|
||||
async fn void_payment(
|
||||
&self,
|
||||
transaction_id: String,
|
||||
payment_data: Option<types::PaymentsCancelData>,
|
||||
) -> types::PaymentsCancelRouterData {
|
||||
let integration = self.get_data().connector.get_connector_integration();
|
||||
let request = generate_data(
|
||||
self.get_name(),
|
||||
self.get_auth_token(),
|
||||
enums::AuthenticationType::NoThreeDs,
|
||||
payment_data.unwrap_or(types::PaymentsCancelData {
|
||||
connector_transaction_id: transaction_id,
|
||||
cancellation_reason: Some("Test cancellation".to_string()),
|
||||
}),
|
||||
);
|
||||
call_connector(request, integration).await
|
||||
}
|
||||
|
||||
async fn refund_payment(
|
||||
&self,
|
||||
transaction_id: String,
|
||||
@ -71,6 +109,29 @@ pub trait ConnectorActions: Connector {
|
||||
let request = generate_data(
|
||||
self.get_name(),
|
||||
self.get_auth_token(),
|
||||
enums::AuthenticationType::NoThreeDs,
|
||||
payment_data.unwrap_or_else(|| types::RefundsData {
|
||||
amount: 100,
|
||||
currency: enums::Currency::USD,
|
||||
refund_id: uuid::Uuid::new_v4().to_string(),
|
||||
payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0),
|
||||
connector_transaction_id: transaction_id,
|
||||
refund_amount: 100,
|
||||
}),
|
||||
);
|
||||
call_connector(request, integration).await
|
||||
}
|
||||
|
||||
async fn sync_refund(
|
||||
&self,
|
||||
transaction_id: String,
|
||||
payment_data: Option<types::RefundsData>,
|
||||
) -> types::RefundSyncRouterData {
|
||||
let integration = self.get_data().connector.get_connector_integration();
|
||||
let request = generate_data(
|
||||
self.get_name(),
|
||||
self.get_auth_token(),
|
||||
enums::AuthenticationType::NoThreeDs,
|
||||
payment_data.unwrap_or_else(|| types::RefundsData {
|
||||
amount: 100,
|
||||
currency: enums::Currency::USD,
|
||||
@ -105,7 +166,32 @@ async fn call_connector<
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub struct MockConfig {
|
||||
pub address: Option<String>,
|
||||
pub mocks: Vec<Mock>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait LocalMock {
|
||||
async fn start_server(&self, config: MockConfig) -> MockServer {
|
||||
let address = config
|
||||
.address
|
||||
.unwrap_or_else(|| "127.0.0.1:9090".to_string());
|
||||
let listener = std::net::TcpListener::bind(address).unwrap();
|
||||
let expected_server_address = listener
|
||||
.local_addr()
|
||||
.expect("Failed to get server address.");
|
||||
let mock_server = MockServer::builder().listener(listener).start().await;
|
||||
assert_eq!(&expected_server_address, mock_server.address());
|
||||
for mock in config.mocks {
|
||||
mock_server.register(mock).await;
|
||||
}
|
||||
mock_server
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PaymentAuthorizeType(pub types::PaymentsAuthorizeData);
|
||||
pub struct PaymentSyncType(pub types::PaymentsSyncData);
|
||||
pub struct PaymentRefundType(pub types::RefundsData);
|
||||
pub struct CCardType(pub api::CCard);
|
||||
|
||||
@ -142,6 +228,18 @@ impl Default for PaymentAuthorizeType {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaymentSyncType {
|
||||
fn default() -> Self {
|
||||
let data = types::PaymentsSyncData {
|
||||
connector_transaction_id: types::ResponseId::ConnectorTransactionId(
|
||||
"12345".to_string(),
|
||||
),
|
||||
encoded_data: None,
|
||||
};
|
||||
Self(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaymentRefundType {
|
||||
fn default() -> Self {
|
||||
let data = types::RefundsData {
|
||||
@ -171,6 +269,7 @@ pub fn get_connector_transaction_id(
|
||||
fn generate_data<Flow, Req: From<Req>, Res>(
|
||||
connector: String,
|
||||
connector_auth_type: types::ConnectorAuthType,
|
||||
auth_type: enums::AuthenticationType,
|
||||
req: Req,
|
||||
) -> types::RouterData<Flow, Req, Res> {
|
||||
types::RouterData {
|
||||
@ -178,9 +277,10 @@ fn generate_data<Flow, Req: From<Req>, Res>(
|
||||
merchant_id: connector.clone(),
|
||||
connector,
|
||||
payment_id: uuid::Uuid::new_v4().to_string(),
|
||||
attempt_id: Some(uuid::Uuid::new_v4().to_string()),
|
||||
status: enums::AttemptStatus::default(),
|
||||
router_return_url: None,
|
||||
auth_type: enums::AuthenticationType::NoThreeDs,
|
||||
auth_type,
|
||||
payment_method: enums::PaymentMethodType::Card,
|
||||
connector_auth_type,
|
||||
description: Some("This is a test".to_string()),
|
||||
|
||||
320
crates/router/tests/connectors/worldpay.rs
Normal file
320
crates/router/tests/connectors/worldpay.rs
Normal file
@ -0,0 +1,320 @@
|
||||
use futures::future::OptionFuture;
|
||||
use router::types::{
|
||||
self,
|
||||
api::{self, enums as api_enums},
|
||||
storage::enums,
|
||||
};
|
||||
use serde_json::json;
|
||||
use serial_test::serial;
|
||||
use wiremock::{
|
||||
matchers::{body_json, method, path},
|
||||
Mock, ResponseTemplate,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
connector_auth,
|
||||
utils::{self, ConnectorActions, LocalMock, MockConfig},
|
||||
};
|
||||
|
||||
struct Worldpay;
|
||||
|
||||
impl LocalMock for Worldpay {}
|
||||
impl ConnectorActions for Worldpay {}
|
||||
impl utils::Connector for Worldpay {
|
||||
fn get_data(&self) -> types::api::ConnectorData {
|
||||
use router::connector::Worldpay;
|
||||
types::api::ConnectorData {
|
||||
connector: Box::new(&Worldpay),
|
||||
connector_name: types::Connector::Worldpay,
|
||||
get_token: types::api::GetToken::Connector,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auth_token(&self) -> types::ConnectorAuthType {
|
||||
types::ConnectorAuthType::from(
|
||||
connector_auth::ConnectorAuthentication::new()
|
||||
.worldpay
|
||||
.expect("Missing connector authentication configuration"),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
"worldpay".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_authorize_card_payment() {
|
||||
let conn = Worldpay {};
|
||||
let _mock = conn.start_server(get_mock_config()).await;
|
||||
let response = conn.authorize_payment(None).await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||
assert_eq!(
|
||||
utils::get_connector_transaction_id(response),
|
||||
Some("123456".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_authorize_gpay_payment() {
|
||||
let conn = Worldpay {};
|
||||
let _mock = conn.start_server(get_mock_config()).await;
|
||||
let response = conn
|
||||
.authorize_payment(Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethod::Wallet(api::WalletData {
|
||||
issuer_name: api_enums::WalletIssuer::GooglePay,
|
||||
token: "someToken".to_string(),
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||
assert_eq!(
|
||||
utils::get_connector_transaction_id(response),
|
||||
Some("123456".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_authorize_applepay_payment() {
|
||||
let conn = Worldpay {};
|
||||
let _mock = conn.start_server(get_mock_config()).await;
|
||||
let response = conn
|
||||
.authorize_payment(Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethod::Wallet(api::WalletData {
|
||||
issuer_name: api_enums::WalletIssuer::ApplePay,
|
||||
token: "someToken".to_string(),
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||
assert_eq!(
|
||||
utils::get_connector_transaction_id(response),
|
||||
Some("123456".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_capture_already_authorized_payment() {
|
||||
let connector = Worldpay {};
|
||||
let _mock = connector.start_server(get_mock_config()).await;
|
||||
let authorize_response = connector.authorize_payment(None).await;
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response);
|
||||
let response: OptionFuture<_> = txn_id
|
||||
.map(|transaction_id| async move {
|
||||
connector.capture_payment(transaction_id, None).await.status
|
||||
})
|
||||
.into();
|
||||
assert_eq!(response.await, Some(enums::AttemptStatus::Charged));
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_sync_payment() {
|
||||
let connector = Worldpay {};
|
||||
let _mock = connector.start_server(get_mock_config()).await;
|
||||
let response = connector
|
||||
.sync_payment(Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
"112233".to_string(),
|
||||
),
|
||||
encoded_data: None,
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_void_already_authorized_payment() {
|
||||
let connector = Worldpay {};
|
||||
let _mock = connector.start_server(get_mock_config()).await;
|
||||
let authorize_response = connector.authorize_payment(None).await;
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response);
|
||||
let response: OptionFuture<_> =
|
||||
txn_id
|
||||
.map(|transaction_id| async move {
|
||||
connector.void_payment(transaction_id, None).await.status
|
||||
})
|
||||
.into();
|
||||
assert_eq!(response.await, Some(enums::AttemptStatus::Voided));
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_fail_capture_for_invalid_payment() {
|
||||
let connector = Worldpay {};
|
||||
let _mock = connector.start_server(get_mock_config()).await;
|
||||
let authorize_response = connector.authorize_payment(None).await;
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
|
||||
let response = connector.capture_payment("12345".to_string(), None).await;
|
||||
let err = response.response.unwrap_err();
|
||||
assert_eq!(
|
||||
err.message,
|
||||
"You must provide valid transaction id to capture payment".to_string()
|
||||
);
|
||||
assert_eq!(err.code, "invalid-id".to_string());
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_refund_succeeded_payment() {
|
||||
let connector = Worldpay {};
|
||||
let _mock = connector.start_server(get_mock_config()).await;
|
||||
//make a successful payment
|
||||
let response = connector.make_payment(None).await;
|
||||
|
||||
//try refund for previous payment
|
||||
let transaction_id = utils::get_connector_transaction_id(response).unwrap();
|
||||
let response = connector.refund_payment(transaction_id, None).await;
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[serial]
|
||||
async fn should_sync_refund() {
|
||||
let connector = Worldpay {};
|
||||
let _mock = connector.start_server(get_mock_config()).await;
|
||||
let response = connector.sync_refund("654321".to_string(), None).await;
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
fn get_mock_config() -> MockConfig {
|
||||
let authorized = json!({
|
||||
"outcome": "authorized",
|
||||
"_links": {
|
||||
"payments:cancel": {
|
||||
"href": "/payments/authorizations/cancellations/123456"
|
||||
},
|
||||
"payments:settle": {
|
||||
"href": "/payments/settlements/123456"
|
||||
},
|
||||
"payments:partialSettle": {
|
||||
"href": "/payments/settlements/partials/123456"
|
||||
},
|
||||
"payments:events": {
|
||||
"href": "/payments/events/123456"
|
||||
},
|
||||
"curies": [
|
||||
{
|
||||
"name": "payments",
|
||||
"href": "/rels/payments/{rel}",
|
||||
"templated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
let settled = json!({
|
||||
"_links": {
|
||||
"payments:refund": {
|
||||
"href": "/payments/settlements/refunds/full/654321"
|
||||
},
|
||||
"payments:partialRefund": {
|
||||
"href": "/payments/settlements/refunds/partials/654321"
|
||||
},
|
||||
"payments:events": {
|
||||
"href": "/payments/events/654321"
|
||||
},
|
||||
"curies": [
|
||||
{
|
||||
"name": "payments",
|
||||
"href": "/rels/payments/{rel}",
|
||||
"templated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
let error_resp = json!({
|
||||
"errorName": "invalid-id",
|
||||
"message": "You must provide valid transaction id to capture payment"
|
||||
});
|
||||
let partial_refund = json!({
|
||||
"_links": {
|
||||
"payments:events": {
|
||||
"href": "https://try.access.worldpay.com/payments/events/eyJrIjoiazNhYjYzMiJ9"
|
||||
},
|
||||
"curies": [{
|
||||
"name": "payments",
|
||||
"href": "https://try.access.worldpay.com/rels/payments/{rel}",
|
||||
"templated": true
|
||||
}]
|
||||
}
|
||||
});
|
||||
let partial_refund_req_body = json!({
|
||||
"value": {
|
||||
"amount": 100,
|
||||
"currency": "USD"
|
||||
},
|
||||
"reference": "123456"
|
||||
});
|
||||
let refunded = json!({
|
||||
"lastEvent": "refunded",
|
||||
"_links": {
|
||||
"payments:cancel": "/payments/authorizations/cancellations/654321",
|
||||
"payments:settle": "/payments/settlements/full/654321",
|
||||
"payments:partialSettle": "/payments/settlements/partials/654321",
|
||||
"curies": [
|
||||
{
|
||||
"name": "payments",
|
||||
"href": "/rels/payments/{rel}",
|
||||
"templated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
let sync_payment = json!({
|
||||
"lastEvent": "authorized",
|
||||
"_links": {
|
||||
"payments:events": "/payments/authorizations/events/654321",
|
||||
"payments:settle": "/payments/settlements/full/654321",
|
||||
"payments:partialSettle": "/payments/settlements/partials/654321",
|
||||
"curies": [
|
||||
{
|
||||
"name": "payments",
|
||||
"href": "/rels/payments/{rel}",
|
||||
"templated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
MockConfig {
|
||||
address: Some("127.0.0.1:9090".to_string()),
|
||||
mocks: vec![
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/payments/authorizations".to_string()))
|
||||
.respond_with(ResponseTemplate::new(201).set_body_json(authorized)),
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/payments/settlements/123456".to_string()))
|
||||
.respond_with(ResponseTemplate::new(202).set_body_json(settled)),
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/payments/events/112233".to_string()))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(sync_payment)),
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/payments/settlements/12345".to_string()))
|
||||
.respond_with(ResponseTemplate::new(400).set_body_json(error_resp)),
|
||||
Mock::given(method("POST"))
|
||||
.and(path(
|
||||
"/payments/settlements/refunds/partials/123456".to_string(),
|
||||
))
|
||||
.and(body_json(partial_refund_req_body))
|
||||
.respond_with(ResponseTemplate::new(202).set_body_json(partial_refund)),
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/payments/events/654321".to_string()))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(refunded)),
|
||||
],
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user