feature(connector): add support for worldpay connector (#272)

This commit is contained in:
Jagan
2023-01-09 12:56:03 +05:30
committed by GitHub
parent 6a0d183e7b
commit 68f92797db
31 changed files with 2198 additions and 104 deletions

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,3 +6,4 @@ mod checkout;
mod connector_auth;
mod shift4;
mod utils;
mod worldpay;

View File

@ -14,4 +14,7 @@ api_key = "Bearer MyApiKey"
key1 = "MyProcessingChannelId"
[shift4]
api_key = "Bearer MyApiKey"
[worldpay]
api_key = "Bearer MyApiKey"

View File

@ -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()),

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