diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 75f2f02c02..f7653f1618 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -303,6 +303,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) .build(), )) } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 65a2ca1133..5ab3ac128c 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -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 diff --git a/crates/router/src/connector/nexinets.rs b/crates/router/src/connector/nexinets.rs index a7224c27a1..596b5cf662 100644 --- a/crates/router/src/connector/nexinets.rs +++ b/crates/router/src/connector/nexinets.rs @@ -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, + ) -> CustomResult, errors::ConnectorError> { + let meta: nexinets::NexinetsPaymentsMetadata = to_connector_meta(connector_meta.clone())?; + Ok(meta.transaction_id) + } +} + impl ConnectorCommonExt for Nexinets where Self: ConnectorIntegration, @@ -44,7 +56,7 @@ where ) -> CustomResult, 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 for Nexinets { @@ -136,10 +153,15 @@ impl ConnectorIntegration CustomResult { - 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 CustomResult { - 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 CustomResult { - 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 CustomResult { - 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 CustomResult { - 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, 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::::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 CustomResult { - 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 for Nexinets { + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, errors::ConnectorError> { + let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; + let nexinets_req = + utils::Encode::::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, 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 { + 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 { + self.build_error_response(res) + } } impl ConnectorIntegration @@ -355,10 +478,17 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - 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, res: Response, ) -> CustomResult, 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 CustomResult { - 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 CustomResult { - 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 for Nexinets +{ + // Not Implemented (R) +} diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index c2b5996084..fadae49747 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -1,48 +1,184 @@ +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, + #[serde(rename = "async")] + nexinets_async: NexinetsAsyncDetails, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct NexinetsCard { - name: Secret, - number: Secret, +#[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), + Wallet(Box), + BankRedirects(Box), +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NexiCardDetails { + #[serde(flatten)] + card_data: CardDataDetails, + cof_contract: Option, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum CardDataDetails { + CardDetails(Box), + PaymentInstrument(Box), +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardDetails { + card_number: Secret, expiry_month: Secret, expiry_year: Secret, - cvc: Secret, - complete: bool, + verification: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentInstrument { + payment_instrument_id: Option, +} + +#[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, + pub cancel_url: Option, + pub failure_url: Option, +} + +#[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), +} + +#[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; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - 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()?, - }; - Ok(Self { - amount: item.request.amount, - card, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + 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 { + initial_amount: item.request.amount, + currency: item.request.currency, + channel: NexinetsChannel::Ecom, + product, + payment, + nexinets_async, + }) } } @@ -55,60 +191,251 @@ impl TryFrom<&types::ConnectorAuthType> for NexinetsAuthType { type Error = error_stack::Report; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { 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 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; + fn try_from(bank: &api_models::enums::BankNames) -> Result { + 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, + payment_instrument: PaymentInstrument, + redirect_url: Option, +} + +#[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, + pub order_id: Option, + pub psync_flow: NexinetsTransactionType, } impl - TryFrom> - for types::RouterData + TryFrom< + types::ResponseRouterData< + F, + NexinetsPreAuthOrDebitResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, - NexinetsPaymentsResponse, + NexinetsPreAuthOrDebitResponse, T, types::PaymentsResponseData, >, ) -> Result { + 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; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + Ok(Self { + initial_amount: item.request.amount_to_capture, + currency: item.request.currency, + }) + } +} + +impl TryFrom<&types::PaymentsCancelRouterData> for NexinetsCaptureOrVoidRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + 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 + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + 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 // 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 TryFrom<&types::RefundsRouterData> for NexinetsRefundRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { 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 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> +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { 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> } } -impl TryFrom> +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { 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> } } -#[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, + pub errors: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct OrderErrorDetails { + pub code: u16, + pub message: String, + pub field: Option, +} + +fn get_payment_details_and_product( + item: &types::PaymentsAuthorizeRouterData, +) -> Result< + (Option, NexinetsProduct), + error_stack::Report, +> { + 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 { + 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 { + 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, NexinetsProduct), + error_stack::Report, +> { + 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> { + 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> { + let transaction_id = meta.transaction_id.clone().ok_or( + errors::ConnectorError::MissingConnectorRelatedTransactionID { + id: "transaction_id".to_string(), + }, + )?; + Ok(transaction_id) } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index a656908146..1bd446a72c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -244,7 +244,7 @@ impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData { pub trait PaymentsSyncRequestData { fn is_auto_capture(&self) -> Result; - fn get_connector_transaction_id(&self) -> CustomResult; + fn get_connector_transaction_id(&self) -> CustomResult; } 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 { + fn get_connector_transaction_id(&self) -> CustomResult { 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 { 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; +} + +impl ApplePay for payments::ApplePayWalletData { + fn get_applepay_decoded_payment_data(&self) -> Result { + 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, Error>; fn get_country_code(&self) -> Result; diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 1377f3019a..b6c653f95a 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -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 }, } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 182ec6efe8..8f8a447494 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -177,7 +177,6 @@ default_imp_for_connector_request_id!( connector::Klarna, connector::Mollie, connector::Multisafepay, - connector::Nexinets, connector::Nuvei, connector::Opennode, connector::Payeezy, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index acbca0f48c..e629cbf2c3 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -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, errors::ApiErrorResponse> { + let metadata = Self::connector_transaction_id(self, &payment_attempt.connector_metadata); + metadata.map_err(|_| errors::ApiErrorResponse::ResourceIdNotFound) + } +} + impl TryFrom> for types::PaymentsCaptureData { type Error = error_stack::Report; diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 4206c29f0e..1a60dd3756 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -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)), diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 7de71b2863..d178c00a0d 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -20,7 +20,7 @@ pub(crate) struct ConnectorAuthentication { pub globalpay: Option, pub mollie: Option, pub multisafepay: Option, - pub nexinets: Option, + pub nexinets: Option, pub nuvei: Option, pub opennode: Option, pub payeezy: Option, diff --git a/crates/router/tests/connectors/nexinets.rs b/crates/router/tests/connectors/nexinets.rs index 93a2432096..86817c2e58 100644 --- a/crates/router/tests/connectors/nexinets.rs +++ b/crates/router/tests/connectors/nexinets.rs @@ -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 { - None +fn payment_method_details() -> Option { + 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 { - 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,19 +377,29 @@ 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!( - refund_response.response.unwrap().refund_status, + response.response.unwrap().refund_status, enums::RefundStatus::Success, ); } @@ -261,31 +407,81 @@ async fn should_partially_refund_succeeded_payment() { // 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; + 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(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); + } } // 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.", ); }