feat(connector) : add Cards(3ds & non3ds),bank_redirects ,wallets(Paypal,Applepay) and Mandates support to nexinets (#898)

Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
This commit is contained in:
chikke srujan
2023-05-03 13:47:42 +05:30
committed by GitHub
parent 2cff019a1b
commit eea05f5c31
11 changed files with 1148 additions and 252 deletions

View File

@ -303,6 +303,7 @@ impl
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}

View File

@ -601,7 +601,7 @@ pub enum Connector {
Klarna,
Mollie,
Multisafepay,
// Nexinets, added as template code for future use
Nexinets,
Nuvei,
// Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage
Paypal,
@ -666,7 +666,7 @@ pub enum RoutableConnectors {
Klarna,
Mollie,
Multisafepay,
// Nexinets, added as template code for future use
Nexinets,
Nuvei,
Opennode,
// Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage

View File

@ -7,12 +7,14 @@ use transformers as nexinets;
use crate::{
configs::settings,
connector::utils::{to_connector_meta, PaymentsSyncRequestData},
core::errors::{self, CustomResult},
headers,
services::{self, ConnectorIntegration},
types::{
self,
api::{self, ConnectorCommon, ConnectorCommonExt},
storage::enums,
ErrorResponse, Response,
},
utils::{self, BytesExt},
@ -33,6 +35,16 @@ impl api::Refund for Nexinets {}
impl api::RefundExecute for Nexinets {}
impl api::RefundSync for Nexinets {}
impl Nexinets {
pub fn connector_transaction_id(
&self,
connector_meta: &Option<serde_json::Value>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let meta: nexinets::NexinetsPaymentsMetadata = to_connector_meta(connector_meta.clone())?;
Ok(meta.transaction_id)
}
}
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Nexinets
where
Self: ConnectorIntegration<Flow, Request, Response>,
@ -44,7 +56,7 @@ where
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
self.get_content_type().to_string(),
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
@ -83,27 +95,32 @@ impl ConnectorCommon for Nexinets {
.parse_struct("NexinetsErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let errors = response.errors.clone();
let mut message = String::new();
for error in errors.iter() {
let field = error.field.to_owned().unwrap_or_default();
let mut msg = String::new();
if !field.is_empty() {
msg.push_str(format!("{} : {}", field, error.message).as_str());
} else {
msg = error.message.to_owned();
}
if message.is_empty() {
message.push_str(&msg);
} else {
message.push_str(format!(", {}", msg).as_str());
}
}
Ok(ErrorResponse {
status_code: res.status_code,
code: response.code,
message: response.message,
reason: response.reason,
status_code: response.status,
code: response.code.to_string(),
message,
reason: Some(response.message),
})
}
}
impl api::PaymentToken for Nexinets {}
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> for Nexinets
{
// Not Implemented (R)
}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Nexinets
{
@ -136,10 +153,15 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let url = if req.request.capture_method == Some(enums::CaptureMethod::Automatic) {
format!("{}/orders/debit", self.base_url(connectors))
} else {
format!("{}/orders/preauth", self.base_url(connectors))
};
Ok(url)
}
fn get_request_body(
@ -178,7 +200,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: &types::PaymentsAuthorizeRouterData,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: nexinets::NexinetsPaymentsResponse = res
let response: nexinets::NexinetsPreAuthOrDebitResponse = res
.response
.parse_struct("Nexinets PaymentsAuthorizeResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -187,7 +209,6 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
@ -215,10 +236,23 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
fn get_url(
&self,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let meta: nexinets::NexinetsPaymentsMetadata =
to_connector_meta(req.request.connector_meta.clone())?;
let order_id = nexinets::get_order_id(&meta)?;
let transaction_id = match meta.psync_flow {
transformers::NexinetsTransactionType::Debit
| transformers::NexinetsTransactionType::Capture => {
req.request.get_connector_transaction_id()?
}
_ => nexinets::get_transaction_id(&meta)?,
};
Ok(format!(
"{}/orders/{order_id}/transactions/{transaction_id}",
self.base_url(connectors)
))
}
fn build_request(
@ -241,16 +275,15 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: nexinets::NexinetsPaymentsResponse = res
let response: nexinets::NexinetsPaymentResponse = res
.response
.parse_struct("nexinets PaymentsSyncResponse")
.parse_struct("nexinets NexinetsPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
@ -278,17 +311,30 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
fn get_url(
&self,
_req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let meta: nexinets::NexinetsPaymentsMetadata =
to_connector_meta(req.request.connector_meta.clone())?;
let order_id = nexinets::get_order_id(&meta)?;
let transaction_id = nexinets::get_transaction_id(&meta)?;
Ok(format!(
"{}/orders/{order_id}/transactions/{transaction_id}/capture",
self.base_url(connectors)
))
}
fn get_request_body(
&self,
_req: &types::PaymentsCaptureRouterData,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into())
let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?;
let nexinets_req =
utils::Encode::<nexinets::NexinetsCaptureOrVoidRequest>::encode_to_string_of_json(
&connector_req,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(nexinets_req))
}
fn build_request(
@ -304,6 +350,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
@ -313,16 +360,15 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
data: &types::PaymentsCaptureRouterData,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: nexinets::NexinetsPaymentsResponse = res
let response: nexinets::NexinetsPaymentResponse = res
.response
.parse_struct("Nexinets PaymentsCaptureResponse")
.parse_struct("NexinetsPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
@ -336,6 +382,83 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Nexinets
{
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 meta: nexinets::NexinetsPaymentsMetadata =
to_connector_meta(req.request.connector_meta.clone())?;
let order_id = nexinets::get_order_id(&meta)?;
let transaction_id = nexinets::get_transaction_id(&meta)?;
Ok(format!(
"{}/orders/{order_id}/transactions/{transaction_id}/cancel",
self.base_url(connectors),
))
}
fn get_request_body(
&self,
req: &types::PaymentsCancelRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?;
let nexinets_req =
utils::Encode::<nexinets::NexinetsCaptureOrVoidRequest>::encode_to_string_of_json(
&connector_req,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(nexinets_req))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.body(types::PaymentsVoidType::get_request_body(self, req)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: nexinets::NexinetsPaymentResponse = res
.response
.parse_struct("NexinetsPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
@ -355,10 +478,17 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
fn get_url(
&self,
_req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let meta: nexinets::NexinetsPaymentsMetadata =
to_connector_meta(req.request.connector_metadata.clone())?;
let order_id = nexinets::get_order_id(&meta)?;
Ok(format!(
"{}/orders/{order_id}/transactions/{}/refund",
self.base_url(connectors),
req.request.connector_transaction_id
))
}
fn get_request_body(
@ -394,7 +524,7 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
data: &types::RefundsRouterData<api::Execute>,
res: Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
let response: nexinets::RefundResponse = res
let response: nexinets::NexinetsRefundResponse = res
.response
.parse_struct("nexinets RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -403,7 +533,6 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
@ -429,10 +558,21 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
fn get_url(
&self,
_req: &types::RefundSyncRouterData,
_connectors: &settings::Connectors,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
let transaction_id = req
.request
.connector_refund_id
.clone()
.ok_or(errors::ConnectorError::MissingConnectorRefundID)?;
let meta: nexinets::NexinetsPaymentsMetadata =
to_connector_meta(req.request.connector_metadata.clone())?;
let order_id = nexinets::get_order_id(&meta)?;
Ok(format!(
"{}/orders/{order_id}/transactions/{transaction_id}",
self.base_url(connectors)
))
}
fn build_request(
@ -446,7 +586,6 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
.body(types::RefundSyncType::get_request_body(self, req)?)
.build(),
))
}
@ -456,7 +595,7 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
data: &types::RefundSyncRouterData,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
let response: nexinets::RefundResponse = res
let response: nexinets::NexinetsRefundResponse = res
.response
.parse_struct("nexinets RefundSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -465,7 +604,6 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
@ -499,3 +637,15 @@ impl api::IncomingWebhook for Nexinets {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
}
}
impl api::PaymentToken for Nexinets {}
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> for Nexinets
{
// Not Implemented (R)
}

View File

@ -1,49 +1,185 @@
use api_models::payments::PaymentMethodData;
use base64::Engine;
use common_utils::errors::CustomResult;
use error_stack::{IntoReport, ResultExt};
use masking::Secret;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
connector::utils::PaymentsAuthorizeRequestData,
connector::utils::{
CardData, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, WalletData,
},
consts,
core::errors,
types::{self, api, storage::enums},
services,
types::{self, api, storage::enums, transformers::ForeignFrom},
};
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsPaymentsRequest {
amount: i64,
card: NexinetsCard,
initial_amount: i64,
currency: enums::Currency,
channel: NexinetsChannel,
product: NexinetsProduct,
payment: Option<NexinetsPaymentDetails>,
#[serde(rename = "async")]
nexinets_async: NexinetsAsyncDetails,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct NexinetsCard {
name: Secret<String>,
number: Secret<String, common_utils::pii::CardNumber>,
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NexinetsChannel {
#[default]
Ecom,
}
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum NexinetsProduct {
#[default]
Creditcard,
Paypal,
Giropay,
Sofort,
Eps,
Ideal,
Applepay,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum NexinetsPaymentDetails {
Card(Box<NexiCardDetails>),
Wallet(Box<NexinetsWalletDetails>),
BankRedirects(Box<NexinetsBankRedirects>),
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NexiCardDetails {
#[serde(flatten)]
card_data: CardDataDetails,
cof_contract: Option<CofContract>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum CardDataDetails {
CardDetails(Box<CardDetails>),
PaymentInstrument(Box<PaymentInstrument>),
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CardDetails {
card_number: Secret<String, common_utils::pii::CardNumber>,
expiry_month: Secret<String>,
expiry_year: Secret<String>,
cvc: Secret<String>,
complete: bool,
verification: Secret<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentInstrument {
payment_instrument_id: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CofContract {
#[serde(rename = "type")]
recurring_type: RecurringType,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RecurringType {
Unscheduled,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsBankRedirects {
bic: NexinetsBIC,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsAsyncDetails {
pub success_url: Option<String>,
pub cancel_url: Option<String>,
pub failure_url: Option<String>,
}
#[derive(Debug, Serialize)]
pub enum NexinetsBIC {
#[serde(rename = "ABNANL2A")]
AbnAmro,
#[serde(rename = "ASNBNL21")]
AsnBank,
#[serde(rename = "BUNQNL2A")]
Bunq,
#[serde(rename = "INGBNL2A")]
Ing,
#[serde(rename = "KNABNL2H")]
Knab,
#[serde(rename = "RABONL2U")]
Rabobank,
#[serde(rename = "RBRBNL21")]
Regiobank,
#[serde(rename = "SNSBNL2A")]
SnsBank,
#[serde(rename = "TRIONL2U")]
TriodosBank,
#[serde(rename = "FVLBNL22")]
VanLanschot,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum NexinetsWalletDetails {
ApplePayToken(Box<ApplePayDetails>),
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplePayDetails {
payment_data: serde_json::Value,
payment_method: ApplepayPaymentMethod,
transaction_identifier: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplepayPaymentMethod {
display_name: String,
network: String,
#[serde(rename = "type")]
token_type: String,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for NexinetsPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(req_card) => {
let card = NexinetsCard {
name: req_card.card_holder_name,
number: req_card.card_number,
expiry_month: req_card.card_exp_month,
expiry_year: req_card.card_exp_year,
cvc: req_card.card_cvc,
complete: item.request.is_auto_capture()?,
let return_url = item.request.router_return_url.clone();
let nexinets_async = NexinetsAsyncDetails {
success_url: return_url.clone(),
cancel_url: return_url.clone(),
failure_url: return_url,
};
let (payment, product) = get_payment_details_and_product(item)?;
Ok(Self {
amount: item.request.amount,
card,
initial_amount: item.request.amount,
currency: item.request.currency,
channel: NexinetsChannel::Ecom,
product,
payment,
nexinets_async,
})
}
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()),
}
}
}
// Auth Struct
@ -55,60 +191,251 @@ impl TryFrom<&types::ConnectorAuthType> for NexinetsAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type {
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self {
api_key: api_key.to_string(),
}),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
types::ConnectorAuthType::BodyKey { api_key, key1 } => {
let auth_key = format!("{key1}:{api_key}");
let auth_header = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_key));
Ok(Self {
api_key: auth_header,
})
}
_ => Err(errors::ConnectorError::FailedToObtainAuthType)?,
}
}
}
// PaymentsResponse
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NexinetsPaymentStatus {
Succeeded,
Failed,
#[default]
Processing,
Success,
Pending,
Ok,
Failure,
Declined,
InProgress,
Expired,
Aborted,
}
impl From<NexinetsPaymentStatus> for enums::AttemptStatus {
fn from(item: NexinetsPaymentStatus) -> Self {
match item {
NexinetsPaymentStatus::Succeeded => Self::Charged,
NexinetsPaymentStatus::Failed => Self::Failure,
NexinetsPaymentStatus::Processing => Self::Authorizing,
impl ForeignFrom<(NexinetsPaymentStatus, NexinetsTransactionType)> for enums::AttemptStatus {
fn foreign_from((status, method): (NexinetsPaymentStatus, NexinetsTransactionType)) -> Self {
match status {
NexinetsPaymentStatus::Success => match method {
NexinetsTransactionType::Preauth => Self::Authorized,
NexinetsTransactionType::Debit | NexinetsTransactionType::Capture => Self::Charged,
NexinetsTransactionType::Cancel => Self::Voided,
},
NexinetsPaymentStatus::Declined
| NexinetsPaymentStatus::Failure
| NexinetsPaymentStatus::Expired
| NexinetsPaymentStatus::Aborted => match method {
NexinetsTransactionType::Preauth => Self::AuthorizationFailed,
NexinetsTransactionType::Debit | NexinetsTransactionType::Capture => {
Self::CaptureFailed
}
NexinetsTransactionType::Cancel => Self::VoidFailed,
},
NexinetsPaymentStatus::Ok => match method {
NexinetsTransactionType::Preauth => Self::Authorized,
_ => Self::Pending,
},
NexinetsPaymentStatus::Pending => Self::AuthenticationPending,
NexinetsPaymentStatus::InProgress => Self::Pending,
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NexinetsPaymentsResponse {
status: NexinetsPaymentStatus,
id: String,
impl TryFrom<&api_models::enums::BankNames> for NexinetsBIC {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(bank: &api_models::enums::BankNames) -> Result<Self, Self::Error> {
match bank {
api_models::enums::BankNames::AbnAmro => Ok(Self::AbnAmro),
api_models::enums::BankNames::AsnBank => Ok(Self::AsnBank),
api_models::enums::BankNames::Bunq => Ok(Self::Bunq),
api_models::enums::BankNames::Ing => Ok(Self::Ing),
api_models::enums::BankNames::Knab => Ok(Self::Knab),
api_models::enums::BankNames::Rabobank => Ok(Self::Rabobank),
api_models::enums::BankNames::Regiobank => Ok(Self::Regiobank),
api_models::enums::BankNames::SnsBank => Ok(Self::SnsBank),
api_models::enums::BankNames::TriodosBank => Ok(Self::TriodosBank),
api_models::enums::BankNames::VanLanschot => Ok(Self::VanLanschot),
_ => Err(errors::ConnectorError::FlowNotSupported {
flow: bank.to_string(),
connector: "Nexinets".to_string(),
}
.into()),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsPreAuthOrDebitResponse {
order_id: String,
transaction_type: NexinetsTransactionType,
transactions: Vec<NexinetsTransaction>,
payment_instrument: PaymentInstrument,
redirect_url: Option<Url>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsTransaction {
pub transaction_id: String,
#[serde(rename = "type")]
pub transaction_type: NexinetsTransactionType,
pub currency: enums::Currency,
pub status: NexinetsPaymentStatus,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NexinetsTransactionType {
Preauth,
Debit,
Capture,
Cancel,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NexinetsPaymentsMetadata {
pub transaction_id: Option<String>,
pub order_id: Option<String>,
pub psync_flow: NexinetsTransactionType,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, NexinetsPaymentsResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
TryFrom<
types::ResponseRouterData<
F,
NexinetsPreAuthOrDebitResponse,
T,
types::PaymentsResponseData,
>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
NexinetsPaymentsResponse,
NexinetsPreAuthOrDebitResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let transaction = match item.response.transactions.first() {
Some(order) => order,
_ => Err(errors::ConnectorError::ResponseHandlingFailed)?,
};
let connector_metadata = serde_json::to_value(NexinetsPaymentsMetadata {
transaction_id: Some(transaction.transaction_id.clone()),
order_id: Some(item.response.order_id.clone()),
psync_flow: item.response.transaction_type.clone(),
})
.into_report()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
let redirection_data = item
.response
.redirect_url
.map(|url| services::RedirectForm::from((url, services::Method::Get)));
let resource_id = match item.response.transaction_type.clone() {
NexinetsTransactionType::Preauth => types::ResponseId::NoResponseId,
NexinetsTransactionType::Debit => {
types::ResponseId::ConnectorTransactionId(transaction.transaction_id.clone())
}
_ => Err(errors::ConnectorError::ResponseHandlingFailed)?,
};
let mandate_reference = item.response.payment_instrument.payment_instrument_id;
Ok(Self {
status: enums::AttemptStatus::from(item.response.status),
status: enums::AttemptStatus::foreign_from((
transaction.status.clone(),
item.response.transaction_type,
)),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
resource_id,
redirection_data,
mandate_reference,
connector_metadata: Some(connector_metadata),
}),
..item.data
})
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsCaptureOrVoidRequest {
pub initial_amount: i64,
pub currency: enums::Currency,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsOrder {
pub order_id: String,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for NexinetsCaptureOrVoidRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
Ok(Self {
initial_amount: item.request.amount_to_capture,
currency: item.request.currency,
})
}
}
impl TryFrom<&types::PaymentsCancelRouterData> for NexinetsCaptureOrVoidRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
Ok(Self {
initial_amount: item.request.get_amount()?,
currency: item.request.get_currency()?,
})
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsPaymentResponse {
pub transaction_id: String,
pub status: NexinetsPaymentStatus,
pub order: NexinetsOrder,
#[serde(rename = "type")]
pub transaction_type: NexinetsTransactionType,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, NexinetsPaymentResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, NexinetsPaymentResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let transaction_id = Some(item.response.transaction_id.clone());
let connector_metadata = serde_json::to_value(NexinetsPaymentsMetadata {
transaction_id,
order_id: Some(item.response.order.order_id),
psync_flow: item.response.transaction_type.clone(),
})
.into_report()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
let resource_id = match item.response.transaction_type.clone() {
NexinetsTransactionType::Debit | NexinetsTransactionType::Capture => {
types::ResponseId::ConnectorTransactionId(item.response.transaction_id)
}
_ => types::ResponseId::NoResponseId,
};
Ok(Self {
status: enums::AttemptStatus::foreign_from((
item.response.status,
item.response.transaction_type,
)),
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
connector_metadata: Some(connector_metadata),
}),
..item.data
})
@ -117,57 +444,71 @@ impl<F, T>
// REFUND :
// Type definition for RefundRequest
#[derive(Default, Debug, Serialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsRefundRequest {
pub amount: i64,
pub initial_amount: i64,
pub currency: enums::Currency,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for NexinetsRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
Ok(Self {
amount: item.request.amount,
initial_amount: item.request.refund_amount,
currency: item.request.currency,
})
}
}
// Type definition for Refund Response
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NexinetsRefundResponse {
pub transaction_id: String,
pub status: RefundStatus,
pub order: NexinetsOrder,
#[serde(rename = "type")]
pub transaction_type: RefundType,
}
#[allow(dead_code)]
#[derive(Debug, Serialize, Default, Deserialize, Clone)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RefundStatus {
Succeeded,
Failed,
#[default]
Processing,
Success,
Ok,
Failure,
Declined,
InProgress,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RefundType {
Refund,
}
impl From<RefundStatus> for enums::RefundStatus {
fn from(item: RefundStatus) -> Self {
match item {
RefundStatus::Succeeded => Self::Success,
RefundStatus::Failed => Self::Failure,
RefundStatus::Processing => Self::Pending,
RefundStatus::Success => Self::Success,
RefundStatus::Failure | RefundStatus::Declined => Self::Failure,
RefundStatus::InProgress | RefundStatus::Ok => Self::Pending,
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct RefundResponse {
id: String,
status: RefundStatus,
}
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
impl TryFrom<types::RefundsResponseRouterData<api::Execute, NexinetsRefundResponse>>
for types::RefundsRouterData<api::Execute>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
item: types::RefundsResponseRouterData<api::Execute, NexinetsRefundResponse>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
connector_refund_id: item.response.transaction_id,
refund_status: enums::RefundStatus::from(item.response.status),
}),
..item.data
@ -175,16 +516,16 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
}
}
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
impl TryFrom<types::RefundsResponseRouterData<api::RSync, NexinetsRefundResponse>>
for types::RefundsRouterData<api::RSync>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
item: types::RefundsResponseRouterData<api::RSync, NexinetsRefundResponse>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id.to_string(),
connector_refund_id: item.response.transaction_id,
refund_status: enums::RefundStatus::from(item.response.status),
}),
..item.data
@ -192,10 +533,153 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
}
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Deserialize)]
pub struct NexinetsErrorResponse {
pub status_code: u16,
pub code: String,
pub status: u16,
pub code: u16,
pub message: String,
pub reason: Option<String>,
pub errors: Vec<OrderErrorDetails>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct OrderErrorDetails {
pub code: u16,
pub message: String,
pub field: Option<String>,
}
fn get_payment_details_and_product(
item: &types::PaymentsAuthorizeRouterData,
) -> Result<
(Option<NexinetsPaymentDetails>, NexinetsProduct),
error_stack::Report<errors::ConnectorError>,
> {
match &item.request.payment_method_data {
PaymentMethodData::Card(card) => Ok((
Some(get_card_data(item, card)?),
NexinetsProduct::Creditcard,
)),
PaymentMethodData::Wallet(wallet) => Ok(get_wallet_details(wallet)?),
PaymentMethodData::BankRedirect(bank_redirect) => match bank_redirect {
api_models::payments::BankRedirectData::Eps { .. } => Ok((None, NexinetsProduct::Eps)),
api_models::payments::BankRedirectData::Giropay { .. } => {
Ok((None, NexinetsProduct::Giropay))
}
api_models::payments::BankRedirectData::Ideal { bank_name, .. } => Ok((
Some(NexinetsPaymentDetails::BankRedirects(Box::new(
NexinetsBankRedirects {
bic: NexinetsBIC::try_from(bank_name)?,
},
))),
NexinetsProduct::Ideal,
)),
api_models::payments::BankRedirectData::Sofort { .. } => {
Ok((None, NexinetsProduct::Sofort))
}
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
))?,
},
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
))?,
}
}
fn get_card_data(
item: &types::PaymentsAuthorizeRouterData,
card: &api_models::payments::Card,
) -> Result<NexinetsPaymentDetails, errors::ConnectorError> {
let (card_data, cof_contract) = match item.request.is_mandate_payment() {
true => {
let card_data = match item.request.off_session {
Some(true) => CardDataDetails::PaymentInstrument(Box::new(PaymentInstrument {
payment_instrument_id: item.request.connector_mandate_id(),
})),
_ => CardDataDetails::CardDetails(Box::new(get_card_details(card))),
};
let cof_contract = Some(CofContract {
recurring_type: RecurringType::Unscheduled,
});
(card_data, cof_contract)
}
false => (
CardDataDetails::CardDetails(Box::new(get_card_details(card))),
None,
),
};
Ok(NexinetsPaymentDetails::Card(Box::new(NexiCardDetails {
card_data,
cof_contract,
})))
}
fn get_applepay_details(
wallet_data: &api_models::payments::WalletData,
applepay_data: &api_models::payments::ApplePayWalletData,
) -> CustomResult<ApplePayDetails, errors::ConnectorError> {
let payment_data = wallet_data.get_wallet_token_as_json()?;
Ok(ApplePayDetails {
payment_data,
payment_method: ApplepayPaymentMethod {
display_name: applepay_data.payment_method.display_name.to_owned(),
network: applepay_data.payment_method.network.to_owned(),
token_type: applepay_data.payment_method.pm_type.to_owned(),
},
transaction_identifier: applepay_data.transaction_identifier.to_owned(),
})
}
fn get_card_details(req_card: &api_models::payments::Card) -> CardDetails {
CardDetails {
card_number: req_card.card_number.clone(),
expiry_month: req_card.card_exp_month.clone(),
expiry_year: req_card.get_card_expiry_year_2_digit(),
verification: req_card.card_cvc.clone(),
}
}
fn get_wallet_details(
wallet: &api_models::payments::WalletData,
) -> Result<
(Option<NexinetsPaymentDetails>, NexinetsProduct),
error_stack::Report<errors::ConnectorError>,
> {
match wallet {
api_models::payments::WalletData::PaypalRedirect(_) => Ok((None, NexinetsProduct::Paypal)),
api_models::payments::WalletData::ApplePay(applepay_data) => Ok((
Some(NexinetsPaymentDetails::Wallet(Box::new(
NexinetsWalletDetails::ApplePayToken(Box::new(get_applepay_details(
wallet,
applepay_data,
)?)),
))),
NexinetsProduct::Applepay,
)),
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
))?,
}
}
pub fn get_order_id(
meta: &NexinetsPaymentsMetadata,
) -> Result<String, error_stack::Report<errors::ConnectorError>> {
let order_id = meta.order_id.clone().ok_or(
errors::ConnectorError::MissingConnectorRelatedTransactionID {
id: "order_id".to_string(),
},
)?;
Ok(order_id)
}
pub fn get_transaction_id(
meta: &NexinetsPaymentsMetadata,
) -> Result<String, error_stack::Report<errors::ConnectorError>> {
let transaction_id = meta.transaction_id.clone().ok_or(
errors::ConnectorError::MissingConnectorRelatedTransactionID {
id: "transaction_id".to_string(),
},
)?;
Ok(transaction_id)
}

View File

@ -244,7 +244,7 @@ impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData {
pub trait PaymentsSyncRequestData {
fn is_auto_capture(&self) -> Result<bool, Error>;
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError>;
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ConnectorError>;
}
impl PaymentsSyncRequestData for types::PaymentsSyncData {
@ -255,14 +255,15 @@ impl PaymentsSyncRequestData for types::PaymentsSyncData {
Some(_) => Err(errors::ConnectorError::CaptureMethodNotSupported.into()),
}
}
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ValidationError> {
fn get_connector_transaction_id(&self) -> CustomResult<String, errors::ConnectorError> {
match self.connector_transaction_id.clone() {
ResponseId::ConnectorTransactionId(txn_id) => Ok(txn_id),
_ => Err(errors::ValidationError::IncorrectValueProvided {
field_name: "connector_transaction_id",
})
.into_report()
.attach_printable("Expected connector transaction ID not found"),
.attach_printable("Expected connector transaction ID not found")
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?,
}
}
}
@ -400,7 +401,7 @@ impl WalletData for api::WalletData {
fn get_wallet_token(&self) -> Result<String, Error> {
match self {
Self::GooglePay(data) => Ok(data.tokenization_data.token.clone()),
Self::ApplePay(data) => Ok(data.payment_data.clone()),
Self::ApplePay(data) => Ok(data.get_applepay_decoded_payment_data()?),
Self::PaypalSdk(data) => Ok(data.token.clone()),
_ => Err(errors::ConnectorError::InvalidWallet.into()),
}
@ -415,6 +416,23 @@ impl WalletData for api::WalletData {
}
}
pub trait ApplePay {
fn get_applepay_decoded_payment_data(&self) -> Result<String, Error>;
}
impl ApplePay for payments::ApplePayWalletData {
fn get_applepay_decoded_payment_data(&self) -> Result<String, Error> {
let token = String::from_utf8(
consts::BASE64_ENGINE
.decode(&self.payment_data)
.into_report()
.change_context(errors::ConnectorError::InvalidWalletToken)?,
)
.into_report()
.change_context(errors::ConnectorError::InvalidWalletToken)?;
Ok(token)
}
}
pub trait PhoneDetailsData {
fn get_number(&self) -> Result<Secret<String>, Error>;
fn get_country_code(&self) -> Result<String, Error>;

View File

@ -295,6 +295,8 @@ pub enum ConnectorError {
MismatchedPaymentData,
#[error("Failed to parse Wallet token")]
InvalidWalletToken,
#[error("Missing Connector Related Transaction ID")]
MissingConnectorRelatedTransactionID { id: String },
#[error("File Validation failed")]
FileValidationFailed { reason: String },
}

View File

@ -177,7 +177,6 @@ default_imp_for_connector_request_id!(
connector::Klarna,
connector::Mollie,
connector::Multisafepay,
connector::Nexinets,
connector::Nuvei,
connector::Opennode,
connector::Payeezy,

View File

@ -6,7 +6,7 @@ use router_env::{instrument, tracing};
use super::{flows::Feature, PaymentAddress, PaymentData};
use crate::{
configs::settings::Server,
connector::Paypal,
connector::{Nexinets, Paypal},
core::{
errors::{self, RouterResponse, RouterResult},
payments::{self, helpers},
@ -581,6 +581,16 @@ impl api::ConnectorTransactionId for Paypal {
}
}
impl api::ConnectorTransactionId for Nexinets {
fn connector_transaction_id(
&self,
payment_attempt: storage::PaymentAttempt,
) -> Result<Option<String>, errors::ApiErrorResponse> {
let metadata = Self::connector_transaction_id(self, &payment_attempt.connector_metadata);
metadata.map_err(|_| errors::ApiErrorResponse::ResourceIdNotFound)
}
}
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureData {
type Error = error_stack::Report<errors::ApiErrorResponse>;

View File

@ -218,7 +218,7 @@ impl ConnectorData {
"worldline" => Ok(Box::new(&connector::Worldline)),
"worldpay" => Ok(Box::new(&connector::Worldpay)),
"multisafepay" => Ok(Box::new(&connector::Multisafepay)),
// "nexinets" => Ok(Box::new(&connector::Nexinets)), added as template code for future use
"nexinets" => Ok(Box::new(&connector::Nexinets)),
"paypal" => Ok(Box::new(&connector::Paypal)),
"trustpay" => Ok(Box::new(&connector::Trustpay)),
"zen" => Ok(Box::new(&connector::Zen)),

View File

@ -20,7 +20,7 @@ pub(crate) struct ConnectorAuthentication {
pub globalpay: Option<HeaderKey>,
pub mollie: Option<HeaderKey>,
pub multisafepay: Option<HeaderKey>,
pub nexinets: Option<HeaderKey>,
pub nexinets: Option<BodyKey>,
pub nuvei: Option<SignatureKey>,
pub opennode: Option<HeaderKey>,
pub payeezy: Option<SignatureKey>,

View File

@ -1,5 +1,5 @@
use masking::Secret;
use router::types::{self, api, storage::enums};
use router::types::{self, api, storage::enums, PaymentsAuthorizeData};
use crate::{
connector_auth,
@ -9,12 +9,13 @@ use crate::{
#[derive(Clone, Copy)]
struct NexinetsTest;
impl ConnectorActions for NexinetsTest {}
static CONNECTOR: NexinetsTest = NexinetsTest {};
impl utils::Connector for NexinetsTest {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Nexinets;
types::api::ConnectorData {
connector: Box::new(&Nexinets),
connector_name: types::Connector::Dummy,
connector_name: types::Connector::Nexinets,
get_token: types::api::GetToken::Connector,
}
}
@ -32,22 +33,23 @@ impl utils::Connector for NexinetsTest {
}
}
static CONNECTOR: NexinetsTest = NexinetsTest {};
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
None
fn payment_method_details() -> Option<PaymentsAuthorizeData> {
Some(PaymentsAuthorizeData {
currency: storage_models::enums::Currency::EUR,
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new("4012001038443335".to_string()),
..utils::CCardType::default().0
}),
router_return_url: Some("https://google.com".to_string()),
..utils::PaymentAuthorizeType::default().0
})
}
fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
None
}
// Cards Positive Tests
// Creates a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.authorize_payment(payment_method_details(), None)
.await
.expect("Authorize payment response");
assert_eq!(response.status, enums::AttemptStatus::Authorized);
@ -57,47 +59,66 @@ async fn should_only_authorize_payment() {
#[actix_web::test]
async fn should_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info())
.authorize_payment(payment_method_details(), None)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Authorized);
let connector_payment_id = "".to_string();
let connector_meta = utils::get_connector_metadata(response.response);
let capture_data = types::PaymentsCaptureData {
connector_meta,
currency: storage_models::enums::Currency::EUR,
..utils::PaymentCaptureType::default().0
};
let capture_response = CONNECTOR
.capture_payment(connector_payment_id, Some(capture_data), None)
.await
.unwrap();
assert_eq!(capture_response.status, enums::AttemptStatus::Charged);
}
// Partially captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_capture_authorized_payment() {
let response = CONNECTOR
.authorize_and_capture_payment(
payment_method_details(),
Some(types::PaymentsCaptureData {
amount_to_capture: 50,
..utils::PaymentCaptureType::default().0
}),
get_default_payment_info(),
)
.authorize_payment(payment_method_details(), None)
.await
.expect("Capture payment response");
assert_eq!(response.status, enums::AttemptStatus::Charged);
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Authorized);
let connector_payment_id = "".to_string();
let connector_meta = utils::get_connector_metadata(response.response);
let capture_data = types::PaymentsCaptureData {
connector_meta,
amount_to_capture: 50,
currency: storage_models::enums::Currency::EUR,
..utils::PaymentCaptureType::default().0
};
let capture_response = CONNECTOR
.capture_payment(connector_payment_id, Some(capture_data), None)
.await
.unwrap();
assert_eq!(capture_response.status, enums::AttemptStatus::Charged);
}
// Synchronizes a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_authorized_payment() {
let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info())
.authorize_payment(payment_method_details(), None)
.await
.expect("Authorize payment response");
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
let txn_id = "".to_string();
let connector_meta = utils::get_connector_metadata(authorize_response.response);
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Authorized,
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
..Default::default()
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id),
encoded_data: None,
capture_method: None,
connector_meta,
}),
get_default_payment_info(),
None,
)
.await
.expect("PSync response");
@ -108,29 +129,63 @@ async fn should_sync_authorized_payment() {
#[actix_web::test]
async fn should_void_authorized_payment() {
let response = CONNECTOR
.authorize_and_void_payment(
payment_method_details(),
.authorize_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Authorized);
let connector_payment_id = "".to_string();
let connector_meta = utils::get_connector_metadata(response.response);
let response = CONNECTOR
.void_payment(
connector_payment_id,
Some(types::PaymentsCancelData {
connector_transaction_id: String::from(""),
cancellation_reason: Some("requested_by_customer".to_string()),
..Default::default()
connector_meta,
amount: Some(100),
currency: Some(storage_models::enums::Currency::EUR),
..utils::PaymentCancelType::default().0
}),
get_default_payment_info(),
None,
)
.await
.expect("Void payment response");
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Voided);
}
// Refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_manually_captured_payment() {
let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), None)
.await
.expect("Authorize payment response");
let txn_id = "".to_string();
let capture_connector_meta = utils::get_connector_metadata(authorize_response.response);
let capture_response = CONNECTOR
.capture_payment(
txn_id,
Some(types::PaymentsCaptureData {
currency: storage_models::enums::Currency::EUR,
connector_meta: capture_connector_meta,
..utils::PaymentCaptureType::default().0
}),
None,
)
.await
.expect("Capture payment response");
let capture_txn_id =
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
let refund_connector_metadata = utils::get_connector_metadata(capture_response.response);
let response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
.refund_payment(
capture_txn_id.clone(),
Some(types::RefundsData {
connector_transaction_id: capture_txn_id,
currency: storage_models::enums::Currency::EUR,
connector_metadata: refund_connector_metadata,
..utils::PaymentRefundType::default().0
}),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
@ -143,15 +198,38 @@ async fn should_refund_manually_captured_payment() {
// Partially refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_manually_captured_payment() {
let response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), None)
.await
.expect("Authorize payment response");
let txn_id = "".to_string();
let capture_connector_meta = utils::get_connector_metadata(authorize_response.response);
let capture_response = CONNECTOR
.capture_payment(
txn_id.clone(),
Some(types::PaymentsCaptureData {
currency: storage_models::enums::Currency::EUR,
connector_meta: capture_connector_meta,
..utils::PaymentCaptureType::default().0
}),
None,
)
.await
.expect("Capture payment response");
let capture_txn_id =
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
let refund_connector_metadata = utils::get_connector_metadata(capture_response.response);
let response = CONNECTOR
.refund_payment(
capture_txn_id.clone(),
Some(types::RefundsData {
refund_amount: 50,
refund_amount: 10,
connector_transaction_id: capture_txn_id,
currency: storage_models::enums::Currency::EUR,
connector_metadata: refund_connector_metadata,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
@ -164,21 +242,63 @@ async fn should_partially_refund_manually_captured_payment() {
// Synchronizes a refund using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_manually_captured_refund() {
let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), None)
.await
.expect("Authorize payment response");
let txn_id = "".to_string();
let capture_connector_meta = utils::get_connector_metadata(authorize_response.response);
let capture_response = CONNECTOR
.capture_payment(
txn_id.clone(),
Some(types::PaymentsCaptureData {
currency: storage_models::enums::Currency::EUR,
connector_meta: capture_connector_meta,
..utils::PaymentCaptureType::default().0
}),
None,
)
.await
.expect("Capture payment response");
let capture_txn_id =
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
let refund_connector_metadata = utils::get_connector_metadata(capture_response.response);
let refund_response = CONNECTOR
.capture_payment_and_refund(
payment_method_details(),
.refund_payment(
capture_txn_id.clone(),
Some(types::RefundsData {
refund_amount: 100,
connector_transaction_id: capture_txn_id.clone(),
currency: storage_models::enums::Currency::EUR,
connector_metadata: refund_connector_metadata.clone(),
..utils::PaymentRefundType::default().0
}),
None,
None,
get_default_payment_info(),
)
.await
.unwrap();
let transaction_id = Some(
refund_response
.response
.clone()
.unwrap()
.connector_refund_id,
);
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
refund_response
.response
.clone()
.unwrap()
.connector_refund_id,
Some(types::RefundsData {
connector_refund_id: transaction_id,
connector_transaction_id: capture_txn_id,
connector_metadata: refund_connector_metadata,
..utils::PaymentRefundType::default().0
}),
None,
get_default_payment_info(),
)
.await
.unwrap();
@ -192,7 +312,7 @@ async fn should_sync_manually_captured_refund() {
#[actix_web::test]
async fn should_make_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
@ -201,24 +321,23 @@ async fn should_make_payment() {
// Synchronizes a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
let cap_response = CONNECTOR
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
assert_eq!(cap_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(cap_response.response.clone()).unwrap();
let connector_meta = utils::get_connector_metadata(cap_response.response);
let response = CONNECTOR
.psync_retry_till_status_matches(
enums::AttemptStatus::Charged,
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id),
capture_method: Some(enums::CaptureMethod::Automatic),
connector_meta,
..Default::default()
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
@ -228,8 +347,25 @@ async fn should_sync_auto_captured_payment() {
// Refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_auto_captured_payment() {
let captured_response = CONNECTOR
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(captured_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(captured_response.response.clone());
let connector_metadata = utils::get_connector_metadata(captured_response.response);
let response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
.refund_payment(
txn_id.clone().unwrap(),
Some(types::RefundsData {
refund_amount: 100,
currency: storage_models::enums::Currency::EUR,
connector_transaction_id: txn_id.unwrap(),
connector_metadata,
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
@ -241,14 +377,55 @@ async fn should_refund_auto_captured_payment() {
// Partially refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_succeeded_payment() {
let refund_response = CONNECTOR
.make_payment_and_refund(
payment_method_details(),
let captured_response = CONNECTOR
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(captured_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(captured_response.response.clone());
let connector_meta = utils::get_connector_metadata(captured_response.response);
let response = CONNECTOR
.refund_payment(
txn_id.clone().unwrap(),
Some(types::RefundsData {
refund_amount: 50,
currency: storage_models::enums::Currency::EUR,
connector_transaction_id: txn_id.unwrap(),
connector_metadata: connector_meta,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
let captured_response = CONNECTOR
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(captured_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(captured_response.response.clone());
let connector_meta = utils::get_connector_metadata(captured_response.response);
for _x in 0..2 {
let refund_response = CONNECTOR
.refund_payment(
txn_id.clone().unwrap(),
Some(types::RefundsData {
connector_metadata: connector_meta.clone(),
connector_transaction_id: txn_id.clone().unwrap(),
refund_amount: 50,
currency: storage_models::enums::Currency::EUR,
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
@ -257,35 +434,54 @@ async fn should_partially_refund_succeeded_payment() {
enums::RefundStatus::Success,
);
}
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
CONNECTOR
.make_payment_and_multiple_refund(
payment_method_details(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
)
.await;
}
// Synchronizes a refund using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_refund() {
let refund_response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
let captured_response = CONNECTOR
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(captured_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(captured_response.response.clone());
let connector_metadata = utils::get_connector_metadata(captured_response.response).clone();
let refund_response = CONNECTOR
.refund_payment(
txn_id.clone().unwrap(),
Some(types::RefundsData {
connector_transaction_id: txn_id.clone().unwrap(),
refund_amount: 100,
currency: storage_models::enums::Currency::EUR,
connector_metadata: connector_metadata.clone(),
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
let transaction_id = Some(
refund_response
.response
.clone()
.unwrap()
.connector_refund_id,
);
let response = CONNECTOR
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
refund_response
.response
.clone()
.unwrap()
.connector_refund_id,
Some(types::RefundsData {
connector_refund_id: transaction_id,
connector_transaction_id: txn_id.unwrap(),
connector_metadata,
..utils::PaymentRefundType::default().0
}),
None,
get_default_payment_info(),
)
.await
.unwrap();
@ -301,20 +497,21 @@ async fn should_sync_refund() {
async fn should_fail_payment_for_incorrect_card_number() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
Some(PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new("1234567891011".to_string()),
card_number: Secret::new("12345678910112331".to_string()),
..utils::CCardType::default().0
}),
currency: storage_models::enums::Currency::EUR,
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card number is incorrect.".to_string(),
"payment.cardNumber : Bad value for 'payment.cardNumber'. Expected: string of length in range 12 <=> 19 representing a valid creditcard number.".to_string(),
);
}
@ -323,21 +520,21 @@ async fn should_fail_payment_for_incorrect_card_number() {
async fn should_fail_payment_for_empty_card_number() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
Some(PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_number: Secret::new(String::from("")),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(
x.message,
"You passed an empty string for 'payment_method_data[card][number]'.",
"payment.cardNumber : Bad value for 'payment.cardNumber'. Expected: string of length in range 12 <=> 19 representing a valid creditcard number.",
);
}
@ -346,20 +543,20 @@ async fn should_fail_payment_for_empty_card_number() {
async fn should_fail_payment_for_incorrect_cvc() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
Some(PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_cvc: Secret::new("12345".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's security code is invalid.".to_string(),
"payment.verification : Bad value for 'payment.verification'. Expected: string of length in range 3 <=> 4 representing a valid creditcard verification number.".to_string(),
);
}
@ -368,20 +565,20 @@ async fn should_fail_payment_for_incorrect_cvc() {
async fn should_fail_payment_for_invalid_exp_month() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
Some(PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_exp_month: Secret::new("20".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration month is invalid.".to_string(),
"payment.expiryMonth : Bad value for 'payment.expiryMonth'. Expected: string of length 2 in range '01' <=> '12' representing the month in a valid creditcard expiry date >= current date.".to_string(),
);
}
@ -390,73 +587,108 @@ async fn should_fail_payment_for_invalid_exp_month() {
async fn should_fail_payment_for_incorrect_expiry_year() {
let response = CONNECTOR
.make_payment(
Some(types::PaymentsAuthorizeData {
Some(PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_exp_year: Secret::new("2000".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Your card's expiration year is invalid.".to_string(),
"payment.expiryYear : Bad value for 'payment.expiryYear'. Expected: string of length 2 in range '01' <=> '99' representing the year in a valid creditcard expiry date >= current date.".to_string(),
);
}
// Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info())
let captured_response = CONNECTOR
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(authorize_response.response);
assert_ne!(txn_id, None, "Empty connector transaction id");
assert_eq!(captured_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(captured_response.response.clone()).unwrap();
let connector_meta = utils::get_connector_metadata(captured_response.response);
let void_response = CONNECTOR
.void_payment(txn_id.unwrap(), None, get_default_payment_info())
.void_payment(
txn_id,
Some(types::PaymentsCancelData {
cancellation_reason: Some("requested_by_customer".to_string()),
amount: Some(100),
currency: Some(storage_models::enums::Currency::EUR),
connector_meta,
..Default::default()
}),
None,
)
.await
.unwrap();
assert_eq!(
void_response.response.unwrap_err().message,
"You cannot cancel this PaymentIntent because it has a status of succeeded."
"transactionId : Operation not allowed!"
);
}
// Captures a payment using invalid connector payment id.
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
let connector_payment_id = "".to_string();
let capture_response = CONNECTOR
.capture_payment("123456789".to_string(), None, get_default_payment_info())
.capture_payment(
connector_payment_id,
Some(types::PaymentsCaptureData {
connector_meta: Some(
serde_json::json!({"transaction_id" : "transaction_usmh41hymb",
"order_id" : "tjil1ymxsz",
"psync_flow" : "PREAUTH"
}),
),
amount_to_capture: 50,
currency: storage_models::enums::Currency::EUR,
..utils::PaymentCaptureType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
capture_response.response.unwrap_err().message,
String::from("No such payment_intent: '123456789'")
String::from("transactionId : Transaction does not belong to order.")
);
}
// Refunds a payment with refund amount higher than payment amount.
#[actix_web::test]
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
let captured_response = CONNECTOR
.make_payment(payment_method_details(), None)
.await
.unwrap();
assert_eq!(captured_response.status, enums::AttemptStatus::Charged);
let txn_id = utils::get_connector_transaction_id(captured_response.response.clone());
let connector_meta = utils::get_connector_metadata(captured_response.response);
let response = CONNECTOR
.make_payment_and_refund(
payment_method_details(),
.refund_payment(
txn_id.clone().unwrap(),
Some(types::RefundsData {
refund_amount: 150,
currency: storage_models::enums::Currency::EUR,
connector_transaction_id: txn_id.unwrap(),
connector_metadata: connector_meta,
..utils::PaymentRefundType::default().0
}),
get_default_payment_info(),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Refund amount (₹1.50) is greater than charge amount (₹1.00)",
"initialAmount : Bad value for 'initialAmount'. Expected: Positive integer between 1 and maximum available amount (debit/capture.initialAmount - debit/capture.refundedAmount.",
);
}