feat(connector): add payment create, sync, capture, refund, void, rsync support for PayU (#351)

Co-authored-by: samraat bansal <samraat.bansal@samraat.bansal-MacBookPro>
This commit is contained in:
SamraatBansal
2023-01-14 13:07:04 +05:30
committed by GitHub
parent 89a1cd0885
commit 55dba87651
14 changed files with 1504 additions and 9 deletions

View File

@ -85,6 +85,8 @@ base_url = "https://api.shift4.com/"
[connectors.worldpay] [connectors.worldpay]
base_url = "http://localhost:9090/" base_url = "http://localhost:9090/"
[connectors.payu]
base_url = "https://secure.snd.payu.com/api/"
[connectors.globalpay] [connectors.globalpay]
base_url = "https://apis.sandbox.globalpay.com/ucp/" base_url = "https://apis.sandbox.globalpay.com/ucp/"

View File

@ -505,6 +505,7 @@ pub enum Connector {
Dummy, Dummy,
Globalpay, Globalpay,
Klarna, Klarna,
Payu,
Shift4, Shift4,
Stripe, Stripe,
Worldpay, Worldpay,

View File

@ -122,17 +122,18 @@ pub struct SupportedConnectors {
pub struct Connectors { pub struct Connectors {
pub aci: ConnectorParams, pub aci: ConnectorParams,
pub adyen: ConnectorParams, pub adyen: ConnectorParams,
pub applepay: ConnectorParams,
pub authorizedotnet: ConnectorParams, pub authorizedotnet: ConnectorParams,
pub braintree: ConnectorParams, pub braintree: ConnectorParams,
pub checkout: ConnectorParams, pub checkout: ConnectorParams,
pub klarna: ConnectorParams,
pub cybersource: ConnectorParams, pub cybersource: ConnectorParams,
pub globalpay: ConnectorParams,
pub klarna: ConnectorParams,
pub payu: ConnectorParams,
pub shift4: ConnectorParams, pub shift4: ConnectorParams,
pub stripe: ConnectorParams, pub stripe: ConnectorParams,
pub supported: SupportedConnectors, pub supported: SupportedConnectors,
pub globalpay: ConnectorParams,
pub worldpay: ConnectorParams, pub worldpay: ConnectorParams,
pub applepay: ConnectorParams,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]

View File

@ -7,6 +7,7 @@ pub mod checkout;
pub mod cybersource; pub mod cybersource;
pub mod globalpay; pub mod globalpay;
pub mod klarna; pub mod klarna;
pub mod payu;
pub mod shift4; pub mod shift4;
pub mod stripe; pub mod stripe;
pub mod utils; pub mod utils;
@ -15,5 +16,5 @@ pub mod worldpay;
pub use self::{ pub use self::{
aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet, aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet,
braintree::Braintree, checkout::Checkout, cybersource::Cybersource, globalpay::Globalpay, braintree::Braintree, checkout::Checkout, cybersource::Cybersource, globalpay::Globalpay,
klarna::Klarna, shift4::Shift4, stripe::Stripe, worldpay::Worldpay, klarna::Klarna, payu::Payu, shift4::Shift4, stripe::Stripe, worldpay::Worldpay,
}; };

View File

@ -0,0 +1,615 @@
mod transformers;
use std::fmt::Debug;
use bytes::Bytes;
use error_stack::{IntoReport, ResultExt};
use transformers as payu;
use crate::{
configs::settings,
core::{
errors::{self, CustomResult},
payments,
},
headers, logger, services,
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
#[derive(Debug, Clone)]
pub struct Payu;
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Payu
where
Self: services::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 header = vec![(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
}
impl ConnectorCommon for Payu {
fn id(&self) -> &'static str {
"payu"
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.payu.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth: payu::PayuAuthType = 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: payu::PayuErrorResponse = res
.parse_struct("Payu ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(ErrorResponse {
code: response.status.status_code,
message: response.status.status_desc,
reason: response.status.code_literal,
})
}
}
impl api::Payment for Payu {}
impl api::PreVerify for Payu {}
impl
services::ConnectorIntegration<
api::Verify,
types::VerifyRequestData,
types::PaymentsResponseData,
> for Payu
{
}
impl api::PaymentVoid for Payu {}
impl
services::ConnectorIntegration<
api::Void,
types::PaymentsCancelData,
types::PaymentsResponseData,
> for Payu
{
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;
Ok(format!(
"{}{}{}",
self.base_url(connectors),
"v2_1/orders/",
connector_payment_id
))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Delete)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: payu::PayuPaymentsCancelResponse = res
.response
.parse_struct("PaymentCancelResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(payments_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::PaymentSync for Payu {}
impl
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Payu
{
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!(
"{}{}{}",
self.base_url(connectors),
"v2_1/orders/",
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)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
logger::debug!(target: "router::connector::payu", response=?res);
let response: payu::PayuPaymentsSyncResponse = res
.response
.parse_struct("payu OrderResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentCapture for Payu {}
impl
services::ConnectorIntegration<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
> for Payu
{
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 get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}{}{}",
self.base_url(connectors),
"v2_1/orders/",
req.request.connector_transaction_id,
"/status"
))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let payu_req = utils::Encode::<payu::PayuPaymentsCaptureRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(payu_req))
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Put)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: payu::PayuPaymentsCaptureResponse = res
.response
.parse_struct("payu CaptureResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::PaymentSession for Payu {}
impl
services::ConnectorIntegration<
api::Session,
types::PaymentsSessionData,
types::PaymentsResponseData,
> for Payu
{
//TODO: implement sessions flow
}
impl api::PaymentAuthorize for Payu {}
impl
services::ConnectorIntegration<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
> for Payu
{
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!("{}{}", self.base_url(connectors), "v2_1/orders"))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let payu_req = utils::Encode::<payu::PayuPaymentsRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(payu_req))
}
fn build_request(
&self,
req: &types::RouterData<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
>,
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: payu::PayuPaymentsResponse = res
.response
.parse_struct("PayuPaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::debug!(payupayments_create_response=?response);
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl api::Refund for Payu {}
impl api::RefundExecute for Payu {}
impl api::RefundSync for Payu {}
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Payu
{
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_url(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}{}{}",
self.base_url(connectors),
"v2_1/orders/",
req.request.connector_transaction_id,
"/refund"
))
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let payu_req = utils::Encode::<payu::PayuRefundRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(payu_req))
}
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::payu", response=?res);
let response: payu::RefundResponse = res
.response
.parse_struct("payu RefundResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
for Payu
{
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!(
"{}{}{}{}",
self.base_url(connectors),
"v2_1/orders/",
req.request.connector_transaction_id,
"/refunds"
))
}
fn build_request(
&self,
req: &types::RefundsRouterData<api::RSync>,
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)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
logger::debug!(target: "router::connector::payu", response=?res);
let response: payu::RefundSyncResponse =
res.response
.parse_struct("payu RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
}
.try_into()
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Payu {
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 Payu {
fn get_flow_type(
&self,
_query_params: &str,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
Ok(payments::CallConnectorAction::Trigger)
}
}

View File

@ -0,0 +1,564 @@
use base64::Engine;
use error_stack::{IntoReport, ResultExt};
use serde::{Deserialize, Serialize};
use crate::{
consts,
core::errors,
pii::{self, Secret},
types::{self, api, storage::enums},
utils::OptionExt,
};
const WALLET_IDENTIFIER: &str = "PBL";
#[derive(Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuPaymentsRequest {
customer_ip: std::net::IpAddr,
merchant_pos_id: String,
total_amount: i64,
currency_code: enums::Currency,
description: String,
pay_methods: PayuPaymentMethod,
continue_url: Option<String>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PayuPaymentMethod {
pay_method: PayuPaymentMethodData,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum PayuPaymentMethodData {
Card(PayuCard),
Wallet(PayuWallet),
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum PayuCard {
#[serde(rename_all = "camelCase")]
Card {
number: Secret<String, pii::CardNumber>,
expiration_month: Secret<String>,
expiration_year: Secret<String>,
cvv: Secret<String>,
},
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PayuWallet {
pub value: PayuWalletCode,
#[serde(rename = "type")]
pub wallet_type: String,
pub authorization_code: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PayuWalletCode {
Ap,
Jp,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayuPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let auth_type = PayuAuthType::try_from(&item.connector_auth_type)?;
let payment_method = match item.request.payment_method_data.clone() {
api::PaymentMethod::Card(ccard) => Ok(PayuPaymentMethod {
pay_method: PayuPaymentMethodData::Card(PayuCard::Card {
number: ccard.card_number,
expiration_month: ccard.card_exp_month,
expiration_year: ccard.card_exp_year,
cvv: ccard.card_cvc,
}),
}),
api::PaymentMethod::Wallet(wallet_data) => match wallet_data.issuer_name {
api_models::enums::WalletIssuer::GooglePay => Ok(PayuPaymentMethod {
pay_method: PayuPaymentMethodData::Wallet({
PayuWallet {
value: PayuWalletCode::Ap,
wallet_type: WALLET_IDENTIFIER.to_string(),
authorization_code: consts::BASE64_ENGINE.encode(
wallet_data
.token
.get_required_value("token")
.change_context(errors::ConnectorError::RequestEncodingFailed)
.attach_printable("No token passed")?,
),
}
}),
}),
api_models::enums::WalletIssuer::ApplePay => Ok(PayuPaymentMethod {
pay_method: PayuPaymentMethodData::Wallet({
PayuWallet {
value: PayuWalletCode::Jp,
wallet_type: WALLET_IDENTIFIER.to_string(),
authorization_code: consts::BASE64_ENGINE.encode(
wallet_data
.token
.get_required_value("token")
.change_context(errors::ConnectorError::RequestEncodingFailed)
.attach_printable("No token passed")?,
),
}
}),
}),
_ => Err(errors::ConnectorError::NotImplemented(
"Unknown Wallet in Payment Method".to_string(),
)),
},
_ => Err(errors::ConnectorError::NotImplemented(
"Unknown payment method".to_string(),
)),
}?;
let browser_info = item.request.browser_info.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "browser_info".to_string(),
},
)?;
Ok(Self {
customer_ip: browser_info.ip_address.ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "browser_info.ip_address".to_string(),
},
)?,
merchant_pos_id: auth_type.merchant_pos_id,
total_amount: item.request.amount,
currency_code: item.request.currency,
description: item.description.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "item.description".to_string(),
},
)?,
pay_methods: payment_method,
continue_url: None,
})
}
}
pub struct PayuAuthType {
pub(super) api_key: String,
pub(super) merchant_pos_id: String,
}
impl TryFrom<&types::ConnectorAuthType> for PayuAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type {
types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self {
api_key: api_key.to_string(),
merchant_pos_id: key1.to_string(),
}),
_ => Err(errors::ConnectorError::FailedToObtainAuthType)?,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PayuPaymentStatus {
Success,
WarningContinueRedirect,
#[serde(rename = "WARNING_CONTINUE_3DS")]
WarningContinue3ds,
WarningContinueCvv,
#[default]
Pending,
}
impl From<PayuPaymentStatus> for enums::AttemptStatus {
fn from(item: PayuPaymentStatus) -> Self {
match item {
PayuPaymentStatus::Success => Self::Pending,
PayuPaymentStatus::WarningContinue3ds => Self::Pending,
PayuPaymentStatus::WarningContinueCvv => Self::Pending,
PayuPaymentStatus::WarningContinueRedirect => Self::Pending,
PayuPaymentStatus::Pending => Self::Pending,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuPaymentsResponse {
pub status: PayuPaymentStatusData,
pub redirect_uri: String,
pub iframe_allowed: Option<bool>,
pub three_ds_protocol_version: Option<String>,
pub order_id: String,
pub ext_order_id: Option<String>,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, PayuPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::ResponseRouterData<F, PayuPaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::from(item.response.status.status_code),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id),
redirect: false,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
amount_captured: None,
..item.data
})
}
}
#[derive(Default, Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuPaymentsCaptureRequest {
order_id: String,
order_status: OrderStatus,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for PayuPaymentsCaptureRequest {
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
Ok(Self {
order_id: item.request.connector_transaction_id.clone(),
order_status: OrderStatus::Completed,
})
}
}
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
pub struct PayuPaymentsCaptureResponse {
status: PayuPaymentStatusData,
}
impl<F, T>
TryFrom<
types::ResponseRouterData<F, PayuPaymentsCaptureResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::ResponseRouterData<
F,
PayuPaymentsCaptureResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::from(item.response.status.status_code.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::NoResponseId,
redirect: false,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
amount_captured: None,
..item.data
})
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuPaymentsCancelResponse {
pub order_id: String,
pub ext_order_id: Option<String>,
pub status: PayuPaymentStatusData,
}
impl<F, T>
TryFrom<
types::ResponseRouterData<F, PayuPaymentsCancelResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::ResponseRouterData<
F,
PayuPaymentsCancelResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: enums::AttemptStatus::from(item.response.status.status_code.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id),
redirect: false,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
amount_captured: None,
..item.data
})
}
}
#[allow(dead_code)]
#[derive(Debug, Serialize, Eq, PartialEq, Default, Deserialize, Clone)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderStatus {
New,
Canceled,
Completed,
WaitingForConfirmation,
#[default]
Pending,
}
impl From<OrderStatus> for enums::AttemptStatus {
fn from(item: OrderStatus) -> Self {
match item {
OrderStatus::New => Self::PaymentMethodAwaited,
OrderStatus::Canceled => Self::Voided,
OrderStatus::Completed => Self::Charged,
OrderStatus::Pending => Self::Pending,
OrderStatus::WaitingForConfirmation => Self::Authorized,
}
}
}
#[derive(Debug, Serialize, Default, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuPaymentStatusData {
status_code: PayuPaymentStatus,
severity: Option<String>,
status_desc: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuProductData {
name: String,
unit_price: String,
quantity: String,
#[serde(rename = "virtual")]
virtually: Option<bool>,
listing_date: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuOrderResponseData {
order_id: String,
ext_order_id: Option<String>,
order_create_date: String,
notify_url: Option<String>,
customer_ip: std::net::IpAddr,
merchant_pos_id: String,
description: String,
validity_time: Option<String>,
currency_code: enums::Currency,
total_amount: String,
buyer: Option<PayuOrderResponseBuyerData>,
pay_method: Option<PayuOrderResponsePayMethod>,
products: Option<Vec<PayuProductData>>,
status: OrderStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuOrderResponseBuyerData {
ext_customer_id: Option<String>,
email: Option<String>,
phone: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
nin: Option<String>,
language: Option<String>,
delivery: Option<String>,
customer_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PayuOrderResponsePayMethod {
CardToken,
Pbl,
Installemnts,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PayuOrderResponseProperty {
name: String,
value: String,
}
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
pub struct PayuPaymentsSyncResponse {
orders: Vec<PayuOrderResponseData>,
status: PayuPaymentStatusData,
properties: Option<Vec<PayuOrderResponseProperty>>,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, PayuPaymentsSyncResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
PayuPaymentsSyncResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let order = match item.response.orders.first() {
Some(order) => order,
_ => Err(errors::ConnectorError::ResponseHandlingFailed)?,
};
Ok(Self {
status: enums::AttemptStatus::from(order.status.clone()),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(order.order_id.clone()),
redirect: false,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
amount_captured: Some(
order
.total_amount
.parse::<i64>()
.into_report()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?,
),
..item.data
})
}
}
#[derive(Default, Debug, Eq, PartialEq, Serialize)]
pub struct PayuRefundRequestData {
description: String,
amount: Option<i64>,
}
#[derive(Default, Debug, Serialize)]
pub struct PayuRefundRequest {
refund: PayuRefundRequestData,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for PayuRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self {
refund: PayuRefundRequestData {
description: item.description.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "item.description".to_string(),
},
)?,
amount: None,
},
})
}
}
// Type definition for Refund Response
#[allow(dead_code)]
#[derive(Debug, Serialize, Eq, PartialEq, Default, Deserialize, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub enum RefundStatus {
Finalized,
Canceled,
#[default]
Pending,
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
match item {
RefundStatus::Finalized => Self::Success,
RefundStatus::Canceled => Self::Failure,
RefundStatus::Pending => Self::Pending,
}
}
}
#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PayuRefundResponseData {
refund_id: String,
ext_refund_id: String,
amount: String,
currency_code: enums::Currency,
description: String,
creation_date_time: String,
status: RefundStatus,
status_date_time: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefundResponse {
refund: PayuRefundResponseData,
}
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
for types::RefundsRouterData<api::Execute>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
) -> Result<Self, Self::Error> {
let refund_status = enums::RefundStatus::from(item.response.refund.status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.refund.refund_id,
refund_status,
}),
..item.data
})
}
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct RefundSyncResponse {
refunds: Vec<PayuRefundResponseData>,
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundSyncResponse>>
for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, RefundSyncResponse>,
) -> Result<Self, Self::Error> {
let refund = match item.response.refunds.first() {
Some(refund) => refund,
_ => Err(errors::ConnectorError::ResponseHandlingFailed)?,
};
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: refund.refund_id.clone(),
refund_status: enums::RefundStatus::from(refund.status.clone()),
}),
..item.data
})
}
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PayuErrorData {
pub status_code: String,
pub code: Option<String>,
pub code_literal: Option<String>,
pub status_desc: String,
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct PayuErrorResponse {
pub status: PayuErrorData,
}

View File

@ -233,6 +233,7 @@ async fn send_request(
logger::debug!(?url_encoded_payload); logger::debug!(?url_encoded_payload);
client.body(url_encoded_payload).send() client.body(url_encoded_payload).send()
} }
// If payload needs processing the body cannot have default
None => client None => client
.body(request.payload.expose_option().unwrap_or_default()) .body(request.payload.expose_option().unwrap_or_default())
.send(), .send(),
@ -240,7 +241,14 @@ async fn send_request(
.await .await
} }
Method::Put => client.put(url).add_headers(headers).send().await, Method::Put => {
client
.put(url)
.add_headers(headers)
.body(request.payload.expose_option().unwrap_or_default()) // If payload needs processing the body cannot have default
.send()
.await
}
Method::Delete => client.delete(url).add_headers(headers).send().await, Method::Delete => client.delete(url).add_headers(headers).send().await,
} }
.map_err(|error| match error { .map_err(|error| match error {
@ -260,7 +268,7 @@ async fn handle_response(
logger::info!(?response); logger::info!(?response);
let status_code = response.status().as_u16(); let status_code = response.status().as_u16();
match status_code { match status_code {
200..=202 => { 200..=202 | 302 => {
logger::debug!(response=?response); logger::debug!(response=?response);
// If needed add log line // If needed add log line
// logger:: error!( error_parsing_response=?err); // logger:: error!( error_parsing_response=?err);

View File

@ -41,7 +41,7 @@ pub(super) fn create_client(
client_certificate: Option<String>, client_certificate: Option<String>,
client_certificate_key: Option<String>, client_certificate_key: Option<String>,
) -> CustomResult<reqwest::Client, errors::ApiClientError> { ) -> CustomResult<reqwest::Client, errors::ApiClientError> {
let mut client_builder = reqwest::Client::builder(); let mut client_builder = reqwest::Client::builder().redirect(reqwest::redirect::Policy::none());
if !should_bypass_proxy { if !should_bypass_proxy {
if let Some(url) = ProxyType::Http.get_proxy_url(proxy) { if let Some(url) = ProxyType::Http.get_proxy_url(proxy) {

View File

@ -150,6 +150,7 @@ impl ConnectorData {
"cybersource" => Ok(Box::new(&connector::Cybersource)), "cybersource" => Ok(Box::new(&connector::Cybersource)),
"shift4" => Ok(Box::new(&connector::Shift4)), "shift4" => Ok(Box::new(&connector::Shift4)),
"worldpay" => Ok(Box::new(&connector::Worldpay)), "worldpay" => Ok(Box::new(&connector::Worldpay)),
"payu" => Ok(Box::new(&connector::Payu)),
"globalpay" => Ok(Box::new(&connector::Globalpay)), "globalpay" => Ok(Box::new(&connector::Globalpay)),
_ => Err(report!(errors::ConnectorError::InvalidConnectorName) _ => Err(report!(errors::ConnectorError::InvalidConnectorName)
.attach_printable(format!("invalid connector name: {connector_name}"))) .attach_printable(format!("invalid connector name: {connector_name}")))

View File

@ -7,6 +7,7 @@ pub(crate) struct ConnectorAuthentication {
pub authorizedotnet: Option<BodyKey>, pub authorizedotnet: Option<BodyKey>,
pub checkout: Option<BodyKey>, pub checkout: Option<BodyKey>,
pub globalpay: Option<HeaderKey>, pub globalpay: Option<HeaderKey>,
pub payu: Option<BodyKey>,
pub shift4: Option<HeaderKey>, pub shift4: Option<HeaderKey>,
pub worldpay: Option<HeaderKey>, pub worldpay: Option<HeaderKey>,
} }

View File

@ -5,6 +5,7 @@ mod authorizedotnet;
mod checkout; mod checkout;
mod connector_auth; mod connector_auth;
mod globalpay; mod globalpay;
mod payu;
mod shift4; mod shift4;
mod utils; mod utils;
mod worldpay; mod worldpay;

View File

@ -0,0 +1,277 @@
use router::types::{self, api, storage::enums};
use crate::{
connector_auth,
utils::{self, ConnectorActions, PaymentAuthorizeType},
};
struct Payu;
impl ConnectorActions for Payu {}
impl utils::Connector for Payu {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Payu;
types::api::ConnectorData {
connector: Box::new(&Payu),
connector_name: types::Connector::Payu,
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.payu
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"payu".to_string()
}
}
#[actix_web::test]
async fn should_authorize_card_payment() {
//Authorize Card Payment in PLN currenct
let authorize_response = Payu {}
.authorize_payment(
Some(types::PaymentsAuthorizeData {
currency: enums::Currency::PLN,
..PaymentAuthorizeType::default().0
}),
None,
)
.await;
// in Payu need Psync to get status therfore set to pending
assert_eq!(authorize_response.status, enums::AttemptStatus::Pending);
if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) {
let sync_response = Payu {}
.sync_payment(
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
transaction_id.clone(),
),
encoded_data: None,
}),
None,
)
.await;
// Assert the sync response, it will be authorized in case of manual capture, for automatic it will be Completed Success
assert_eq!(sync_response.status, enums::AttemptStatus::Authorized);
}
}
#[actix_web::test]
async fn should_authorize_gpay_payment() {
let authorize_response = Payu {}.authorize_payment(Some(types::PaymentsAuthorizeData{
payment_method_data: types::api::PaymentMethod::Wallet(api::WalletData{
issuer_name: api_models::enums::WalletIssuer::GooglePay,
token: Some(r#"{"signature":"MEUCIQD7Ta+d9+buesrH2KKkF+03AqTen+eHHN8KFleHoKaiVAIgGvAXyI0Vg3ws8KlF7agW/gmXJhpJOOPkqiNVbn/4f0Y\u003d","protocolVersion":"ECv1","signedMessage":"{\"encryptedMessage\":\"UcdGP9F/1loU0aXvVj6VqGRPA5EAjHYfJrXD0N+5O13RnaJXKWIjch1zzjpy9ONOZHqEGAqYKIcKcpe5ppN4Fpd0dtbm1H4u+lA+SotCff3euPV6sne22/Pl/MNgbz5QvDWR0UjcXvIKSPNwkds1Ib7QMmH4GfZ3vvn6s534hxAmcv/LlkeM4FFf6py9crJK5fDIxtxRJncfLuuPeAXkyy+u4zE33HmT34Oe5MSW/kYZVz31eWqFy2YCIjbJcC9ElMluoOKSZ305UG7tYGB1LCFGQLtLxphrhPu1lEmGEZE1t2cVDoCzjr3rm1OcfENc7eNC4S+ko6yrXh1ZX06c/F9kunyLn0dAz8K5JLIwLdjw3wPADVSd3L0eM7jkzhH80I6nWkutO0x8BFltxWl+OtzrnAe093OUncH6/DK1pCxtJaHdw1WUWrzULcdaMZmPfA\\u003d\\u003d\",\"ephemeralPublicKey\":\"BH7A1FUBWiePkjh/EYmsjY/63D/6wU+4UmkLh7WW6v7PnoqQkjrFpc4kEP5a1Op4FkIlM9LlEs3wGdFB8xIy9cM\\u003d\",\"tag\":\"e/EOsw2Y2wYpJngNWQqH7J62Fhg/tzmgDl6UFGuAN+A\\u003d\"}"}"#
.to_string()) //Generate new GooglePay token this is bound to expire
}),
currency: enums::Currency::PLN,
..PaymentAuthorizeType::default().0
}), None).await;
assert_eq!(authorize_response.status, enums::AttemptStatus::Pending);
if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) {
let sync_response = Payu {}
.sync_payment(
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
transaction_id.clone(),
),
encoded_data: None,
}),
None,
)
.await;
assert_eq!(sync_response.status, enums::AttemptStatus::Authorized);
}
}
#[actix_web::test]
async fn should_capture_already_authorized_payment() {
let connector = Payu {};
let authorize_response = connector
.authorize_payment(
Some(types::PaymentsAuthorizeData {
currency: enums::Currency::PLN,
..PaymentAuthorizeType::default().0
}),
None,
)
.await;
assert_eq!(authorize_response.status, enums::AttemptStatus::Pending);
if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) {
let sync_response = connector
.sync_payment(
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
transaction_id.clone(),
),
encoded_data: None,
}),
None,
)
.await;
assert_eq!(sync_response.status, enums::AttemptStatus::Authorized);
let capture_response = connector
.capture_payment(transaction_id.clone(), None, None)
.await;
assert_eq!(capture_response.status, enums::AttemptStatus::Pending);
let response = connector
.sync_payment(
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
transaction_id,
),
encoded_data: None,
}),
None,
)
.await;
assert_eq!(response.status, enums::AttemptStatus::Charged,);
}
}
#[actix_web::test]
async fn should_sync_payment() {
let connector = Payu {};
// Authorize the payment for manual capture
let authorize_response = connector
.authorize_payment(
Some(types::PaymentsAuthorizeData {
currency: enums::Currency::PLN,
..PaymentAuthorizeType::default().0
}),
None,
)
.await;
assert_eq!(authorize_response.status, enums::AttemptStatus::Pending);
if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) {
// Sync the Payment Data
let response = connector
.sync_payment(
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
transaction_id,
),
encoded_data: None,
}),
None,
)
.await;
assert_eq!(response.status, enums::AttemptStatus::Authorized);
}
}
#[actix_web::test]
async fn should_void_already_authorized_payment() {
let connector = Payu {};
//make a successful payment
let authorize_response = connector
.make_payment(
Some(types::PaymentsAuthorizeData {
currency: enums::Currency::PLN,
..PaymentAuthorizeType::default().0
}),
None,
)
.await;
assert_eq!(authorize_response.status, enums::AttemptStatus::Pending);
//try CANCEL for previous payment
if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) {
let void_response = connector
.void_payment(transaction_id.clone(), None, None)
.await;
assert_eq!(void_response.status, enums::AttemptStatus::Pending);
let sync_response = connector
.sync_payment(
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
transaction_id,
),
encoded_data: None,
}),
None,
)
.await;
assert_eq!(sync_response.status, enums::AttemptStatus::Voided,);
}
}
#[actix_web::test]
async fn should_refund_succeeded_payment() {
let connector = Payu {};
//make a successful payment
let authorize_response = connector
.make_payment(
Some(types::PaymentsAuthorizeData {
currency: enums::Currency::PLN,
..PaymentAuthorizeType::default().0
}),
None,
)
.await;
assert_eq!(authorize_response.status, enums::AttemptStatus::Pending);
if let Some(transaction_id) = utils::get_connector_transaction_id(authorize_response) {
//Capture the payment in case of Manual Capture
let capture_response = connector
.capture_payment(transaction_id.clone(), None, None)
.await;
assert_eq!(capture_response.status, enums::AttemptStatus::Pending);
let sync_response = connector
.sync_payment(
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
transaction_id.clone(),
),
encoded_data: None,
}),
None,
)
.await;
assert_eq!(sync_response.status, enums::AttemptStatus::Charged);
//Refund the payment
let refund_response = connector
.refund_payment(transaction_id.clone(), None, None)
.await;
assert_eq!(
refund_response.response.unwrap().connector_refund_id.len(),
10
);
}
}
#[actix_web::test]
async fn should_sync_succeeded_refund_payment() {
let connector = Payu {};
//Currently hardcoding the order_id because RSync is not instant, change it accordingly
let sync_refund_response = connector
.sync_refund("6DHQQN3T57230110GUEST000P01".to_string(), None, None)
.await;
assert_eq!(
sync_refund_response.response.unwrap().refund_status,
enums::RefundStatus::Success
);
}
#[actix_web::test]
async fn should_fail_already_refunded_payment() {
let connector = Payu {};
//Currently hardcoding the order_id, change it accordingly
let response = connector
.refund_payment("5H1SVX6P7W230112GUEST000P01".to_string(), None, None)
.await;
let x = response.response.unwrap_err();
assert_eq!(x.reason.unwrap(), "PAID".to_string());
}

View File

@ -19,5 +19,9 @@ api_key = "Bearer MyApiKey"
[worldpay] [worldpay]
api_key = "Bearer MyApiKey" api_key = "Bearer MyApiKey"
[payu]
api_key = "Bearer MyApiKey"
key1 = "MerchantPosId"
[globalpay] [globalpay]
api_key = "Bearer MyApiKey" api_key = "Bearer MyApiKey"

View File

@ -135,7 +135,7 @@ pub trait ConnectorActions: Connector {
let integration = self.get_data().connector.get_connector_integration(); let integration = self.get_data().connector.get_connector_integration();
let request = self.generate_data( let request = self.generate_data(
payment_data.unwrap_or_else(|| types::RefundsData { payment_data.unwrap_or_else(|| types::RefundsData {
amount: 100, amount: 1000,
currency: enums::Currency::USD, currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(), refund_id: uuid::Uuid::new_v4().to_string(),
connector_transaction_id: transaction_id, connector_transaction_id: transaction_id,
@ -230,6 +230,7 @@ pub struct PaymentAuthorizeType(pub types::PaymentsAuthorizeData);
pub struct PaymentSyncType(pub types::PaymentsSyncData); pub struct PaymentSyncType(pub types::PaymentsSyncData);
pub struct PaymentRefundType(pub types::RefundsData); pub struct PaymentRefundType(pub types::RefundsData);
pub struct CCardType(pub api::CCard); pub struct CCardType(pub api::CCard);
pub struct BrowserInfoType(pub types::BrowserInformation);
impl Default for CCardType { impl Default for CCardType {
fn default() -> Self { fn default() -> Self {
@ -256,7 +257,7 @@ impl Default for PaymentAuthorizeType {
mandate_id: None, mandate_id: None,
off_session: None, off_session: None,
setup_mandate_details: None, setup_mandate_details: None,
browser_info: None, browser_info: Some(BrowserInfoType::default().0),
order_details: None, order_details: None,
email: None, email: None,
}; };
@ -264,6 +265,24 @@ impl Default for PaymentAuthorizeType {
} }
} }
impl Default for BrowserInfoType {
fn default() -> Self {
let data = types::BrowserInformation {
user_agent: "".to_string(),
accept_header: "".to_string(),
language: "nl-NL".to_string(),
color_depth: 24,
screen_height: 723,
screen_width: 1536,
time_zone: 0,
java_enabled: true,
java_script_enabled: true,
ip_address: Some("127.0.0.1".parse().unwrap()),
};
Self(data)
}
}
impl Default for PaymentSyncType { impl Default for PaymentSyncType {
fn default() -> Self { fn default() -> Self {
let data = types::PaymentsSyncData { let data = types::PaymentsSyncData {