mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 11:24:45 +08:00
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:
@ -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/"
|
||||||
|
|
||||||
|
|||||||
@ -505,6 +505,7 @@ pub enum Connector {
|
|||||||
Dummy,
|
Dummy,
|
||||||
Globalpay,
|
Globalpay,
|
||||||
Klarna,
|
Klarna,
|
||||||
|
Payu,
|
||||||
Shift4,
|
Shift4,
|
||||||
Stripe,
|
Stripe,
|
||||||
Worldpay,
|
Worldpay,
|
||||||
|
|||||||
@ -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)]
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
615
crates/router/src/connector/payu.rs
Normal file
615
crates/router/src/connector/payu.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
564
crates/router/src/connector/payu/transformers.rs
Normal file
564
crates/router/src/connector/payu/transformers.rs
Normal 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,
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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}")))
|
||||||
|
|||||||
@ -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>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
277
crates/router/tests/connectors/payu.rs
Normal file
277
crates/router/tests/connectors/payu.rs
Normal 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());
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user