diff --git a/crates/hyperswitch_connectors/src/connectors/nuvei.rs b/crates/hyperswitch_connectors/src/connectors/nuvei.rs index 259495d0545..9c906abeefc 100644 --- a/crates/hyperswitch_connectors/src/connectors/nuvei.rs +++ b/crates/hyperswitch_connectors/src/connectors/nuvei.rs @@ -53,6 +53,7 @@ use masking::ExposeInterface; use transformers as nuvei; use crate::{ + connectors::nuvei::transformers::{NuveiPaymentsResponse, NuveiTransactionSyncResponse}, constants::headers, types::ResponseRouterData, utils::{self, is_mandate_supported, PaymentMethodDataType, RouterData as _}, @@ -211,7 +212,7 @@ impl ConnectorIntegration, errors::ConnectorError, > { - let response: nuvei::NuveiPaymentsResponse = res + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; @@ -234,72 +235,68 @@ impl ConnectorIntegration - for Nuvei -{ +impl ConnectorIntegration for Nuvei { fn get_headers( &self, - req: &PaymentsCompleteAuthorizeRouterData, + req: &PaymentsCancelRouterData, connectors: &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: &PaymentsCompleteAuthorizeRouterData, + _req: &PaymentsCancelRouterData, connectors: &Connectors, ) -> CustomResult { Ok(format!( - "{}ppp/api/v1/payment.do", + "{}ppp/api/v1/voidTransaction.do", ConnectorCommon::base_url(self, connectors) )) } + fn get_request_body( &self, - req: &PaymentsCompleteAuthorizeRouterData, + req: &PaymentsCancelRouterData, _connectors: &Connectors, ) -> CustomResult { - let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?; - let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; + let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; Ok(RequestContent::Json(Box::new(connector_req))) } + fn build_request( &self, - req: &PaymentsCompleteAuthorizeRouterData, + req: &PaymentsCancelRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsCompleteAuthorizeType::get_url( - self, req, connectors, - )?) - .attach_default_headers() - .headers(types::PaymentsCompleteAuthorizeType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( - self, req, connectors, - )?) - .build(), - )) + let request = RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) } + fn handle_response( &self, - data: &PaymentsCompleteAuthorizeRouterData, + data: &PaymentsCancelRouterData, event_builder: Option<&mut ConnectorEvent>, res: Response, - ) -> CustomResult { - let response: nuvei::NuveiPaymentsResponse = res + ) -> CustomResult { + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); - RouterData::try_from(ResponseRouterData { response, data: data.clone(), @@ -317,63 +314,66 @@ impl ConnectorIntegration for Nuvei { +impl ConnectorIntegration + for Nuvei +{ fn get_headers( &self, - req: &PaymentsCancelRouterData, + req: &PaymentsCompleteAuthorizeRouterData, connectors: &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: &PaymentsCancelRouterData, + _req: &PaymentsCompleteAuthorizeRouterData, connectors: &Connectors, ) -> CustomResult { Ok(format!( - "{}ppp/api/v1/voidTransaction.do", + "{}ppp/api/v1/payment.do", ConnectorCommon::base_url(self, connectors) )) } - fn get_request_body( &self, - req: &PaymentsCancelRouterData, + req: &PaymentsCompleteAuthorizeRouterData, _connectors: &Connectors, ) -> CustomResult { - let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?; + let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; Ok(RequestContent::Json(Box::new(connector_req))) } - fn build_request( &self, - req: &PaymentsCancelRouterData, + req: &PaymentsCompleteAuthorizeRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - let request = RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .set_body(types::PaymentsVoidType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } - fn handle_response( &self, - data: &PaymentsCancelRouterData, + data: &PaymentsCompleteAuthorizeRouterData, event_builder: Option<&mut ConnectorEvent>, res: Response, - ) -> CustomResult { - let response: nuvei::NuveiPaymentsResponse = res + ) -> CustomResult { + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; @@ -458,7 +458,7 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: nuvei::NuveiPaymentsResponse = res + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; @@ -501,7 +501,7 @@ impl ConnectorIntegration for Nuv connectors: &Connectors, ) -> CustomResult { Ok(format!( - "{}ppp/api/v1/getPaymentStatus.do", + "{}ppp/api/v1/getTransactionDetails.do", ConnectorCommon::base_url(self, connectors) )) } @@ -546,9 +546,9 @@ impl ConnectorIntegration for Nuv event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: nuvei::NuveiPaymentsResponse = res + let response: NuveiTransactionSyncResponse = res .response - .parse_struct("NuveiPaymentsResponse") + .parse_struct("NuveiTransactionSyncResponse") .switch()?; event_builder.map(|i| i.set_response_body(&response)); @@ -622,7 +622,7 @@ impl ConnectorIntegration fo event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: nuvei::NuveiPaymentsResponse = res + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; @@ -678,7 +678,6 @@ impl ConnectorIntegration CustomResult { let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - Ok(RequestContent::Json(Box::new(connector_req))) } @@ -710,7 +709,7 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: nuvei::NuveiPaymentsResponse = res + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; @@ -883,13 +882,12 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: nuvei::NuveiPaymentsResponse = res + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); - RouterData::try_from(ResponseRouterData { response, data: data.clone(), @@ -965,7 +963,7 @@ impl ConnectorIntegration for Nuvei { event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: nuvei::NuveiPaymentsResponse = res + let response: NuveiPaymentsResponse = res .response .parse_struct("NuveiPaymentsResponse") .switch()?; @@ -1124,10 +1122,8 @@ impl IncomingWebhook for Nuvei { // Parse the webhook payload let webhook = serde_urlencoded::from_str::(&request.query_params) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - // Convert webhook to payments response - let payment_response = nuvei::NuveiPaymentsResponse::from(webhook); - + let payment_response = NuveiPaymentsResponse::from(webhook); Ok(Box::new(payment_response)) } } diff --git a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs index 0e032c3c804..2092fc40a20 100644 --- a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs @@ -49,7 +49,8 @@ use crate::{ utils::{ self, convert_amount, missing_field_err, AddressData, AddressDetailsData, BrowserInformationData, ForeignTryFrom, PaymentsAuthorizeRequestData, - PaymentsCancelRequestData, PaymentsPreProcessingRequestData, RouterData as _, + PaymentsCancelRequestData, PaymentsPreProcessingRequestData, + PaymentsSetupMandateRequestData, RouterData as _, }, }; @@ -136,9 +137,7 @@ impl NuveiAuthorizePreprocessingCommon for SetupMandateRequestData { fn get_return_url_required( &self, ) -> Result> { - self.router_return_url - .clone() - .ok_or_else(missing_field_err("return_url")) + self.get_router_return_url() } fn get_capture_method(&self) -> Option { @@ -201,10 +200,6 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsAuthorizeData { self.customer_id.clone() } - fn get_complete_authorize_url(&self) -> Option { - self.complete_authorize_url.clone() - } - fn get_connector_mandate_id(&self) -> Option { self.connector_mandate_id().clone() } @@ -219,6 +214,10 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsAuthorizeData { self.capture_method } + fn get_complete_authorize_url(&self) -> Option { + self.complete_authorize_url.clone() + } + fn get_minor_amount_required( &self, ) -> Result> { @@ -273,10 +272,6 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsPreProcessingData { && self.setup_future_usage == Some(FutureUsage::OffSession) } - fn get_complete_authorize_url(&self) -> Option { - self.complete_authorize_url.clone() - } - fn get_connector_mandate_id(&self) -> Option { self.connector_mandate_id() } @@ -291,6 +286,10 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsPreProcessingData { self.capture_method } + fn get_complete_authorize_url(&self) -> Option { + self.complete_authorize_url.clone() + } + fn get_minor_amount_required( &self, ) -> Result> { @@ -502,7 +501,11 @@ pub struct NuveiPaymentFlowRequest { #[derive(Debug, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct NuveiPaymentSyncRequest { - pub session_token: Secret, + pub merchant_id: Secret, + pub merchant_site_id: Secret, + pub time_stamp: String, + pub checksum: Secret, + pub transaction_id: String, } #[derive(Debug, Default, Serialize, Deserialize)] @@ -848,9 +851,9 @@ pub struct NuveiACSResponse { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LiabilityShift { - #[serde(rename = "Y", alias = "1")] + #[serde(rename = "Y", alias = "1", alias = "y")] Success, - #[serde(rename = "N", alias = "0")] + #[serde(rename = "N", alias = "0", alias = "n")] Failed, } @@ -1724,8 +1727,8 @@ where let shipping_address: Option = item.get_optional_shipping().map(|address| address.into()); - let billing_address: Option = address.map(|ref address| address.into()); - + let billing_address: Option = + address.clone().map(|ref address| address.into()); let device_details = if request_data .device_details .ip_address @@ -1902,25 +1905,7 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, Secret)> ..Default::default() }) } - Some(PaymentMethodData::Wallet(..)) - | Some(PaymentMethodData::PayLater(..)) - | Some(PaymentMethodData::BankDebit(..)) - | Some(PaymentMethodData::BankRedirect(..)) - | Some(PaymentMethodData::BankTransfer(..)) - | Some(PaymentMethodData::Crypto(..)) - | Some(PaymentMethodData::MandatePayment) - | Some(PaymentMethodData::GiftCard(..)) - | Some(PaymentMethodData::Voucher(..)) - | Some(PaymentMethodData::CardRedirect(..)) - | Some(PaymentMethodData::Reward) - | Some(PaymentMethodData::RealTimePayment(..)) - | Some(PaymentMethodData::MobilePayment(..)) - | Some(PaymentMethodData::Upi(..)) - | Some(PaymentMethodData::OpenBanking(_)) - | Some(PaymentMethodData::CardToken(..)) - | Some(PaymentMethodData::NetworkToken(..)) - | Some(PaymentMethodData::CardDetailsForNetworkTransactionId(_)) - | None => Err(errors::ConnectorError::NotImplemented( + _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), )), }?; @@ -1938,7 +1923,7 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, Secret)> ..Default::default() })?; Ok(Self { - related_transaction_id: request_data.related_transaction_id, + related_transaction_id: item.request.connector_transaction_id.clone(), payment_option: request_data.payment_option, device_details: request_data.device_details, ..request @@ -2071,9 +2056,33 @@ impl TryFrom<&types::RefundExecuteRouterData> for NuveiPaymentFlowRequest { impl TryFrom<&types::PaymentsSyncRouterData> for NuveiPaymentSyncRequest { type Error = error_stack::Report; fn try_from(value: &types::PaymentsSyncRouterData) -> Result { - let meta: NuveiMeta = utils::to_connector_meta(value.request.connector_meta.clone())?; + let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&value.connector_auth_type)?; + let merchant_id = connector_meta.merchant_id.clone(); + let merchant_site_id = connector_meta.merchant_site_id.clone(); + let merchant_secret = connector_meta.merchant_secret.clone(); + let time_stamp = + date_time::format_date(date_time::now(), date_time::DateFormat::YYYYMMDDHHmmss) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let transaction_id = value + .request + .connector_transaction_id + .clone() + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + let checksum = Secret::new(encode_payload(&[ + merchant_id.peek(), + merchant_site_id.peek(), + &transaction_id, + &time_stamp, + merchant_secret.peek(), + ])?); + Ok(Self { - session_token: meta.session_token, + merchant_id, + merchant_site_id, + time_stamp, + checksum, + transaction_id, }) } } @@ -2189,11 +2198,17 @@ pub enum NuveiPaymentStatus { #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "UPPERCASE")] pub enum NuveiTransactionStatus { + #[serde(alias = "Approved", alias = "APPROVED")] Approved, + #[serde(alias = "Declined", alias = "DECLINED")] Declined, + #[serde(alias = "Filter Error", alias = "ERROR", alias = "Error")] Error, + #[serde(alias = "Redirect", alias = "REDIRECT")] Redirect, + #[serde(alias = "Pending", alias = "PENDING")] Pending, + #[serde(alias = "Processing", alias = "PROCESSING")] #[default] Processing, } @@ -2251,35 +2266,105 @@ pub struct NuveiPaymentsResponse { pub client_request_id: Option, pub merchant_advice_code: Option, } -impl NuveiPaymentsResponse { - /// returns amount_captured and minor_amount_capturable - pub fn get_amount_captured( - &self, - ) -> Result<(Option, Option), error_stack::Report> { + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiTxnPartialApproval { + requested_amount: Option, + requested_currency: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiTransactionSyncResponseDetails { + gw_error_code: Option, + gw_error_reason: Option, + gw_extended_error_code: Option, + transaction_id: Option, + transaction_status: Option, + transaction_type: Option, + auth_code: Option, + processed_amount: Option, + processed_currency: Option, + acquiring_bank_name: Option, +} +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiTransactionSyncResponse { + pub payment_option: Option, + pub partial_approval: Option, + pub is_currency_converted: Option, + pub transaction_details: Option, + pub fraud_details: Option, + pub client_unique_id: Option, + pub internal_request_id: Option, + pub status: NuveiPaymentStatus, + pub err_code: Option, + pub reason: Option, + pub merchant_id: Option>, + pub merchant_site_id: Option>, + pub version: Option, + pub client_request_id: Option, + pub merchant_advice_code: Option, +} +impl NuveiTransactionSyncResponse { + pub fn get_partial_approval(&self) -> Option { match &self.partial_approval { - Some(partial_approval) => { - let amount = utils::convert_back_amount_to_minor_units( - NUVEI_AMOUNT_CONVERTOR, - partial_approval.processed_amount.clone(), - partial_approval.processed_currency, - )?; - match self.transaction_type { - None => Ok((None, None)), - Some(NuveiTransactionType::Sale) => { - Ok((Some(MinorUnit::get_amount_as_i64(amount)), None)) - } - Some(NuveiTransactionType::Auth) => Ok((None, Some(amount))), - Some(NuveiTransactionType::Auth3D) => { - Ok((Some(MinorUnit::get_amount_as_i64(amount)), None)) - } - Some(NuveiTransactionType::InitAuth3D) => Ok((None, Some(amount))), - Some(NuveiTransactionType::Credit) => Ok((None, None)), - Some(NuveiTransactionType::Void) => Ok((None, None)), - Some(NuveiTransactionType::Settle) => Ok((None, None)), + Some(partial_approval) => match ( + partial_approval.requested_amount.clone(), + partial_approval.requested_currency, + self.transaction_details + .as_ref() + .and_then(|txn| txn.processed_amount.clone()), + self.transaction_details + .as_ref() + .and_then(|txn| txn.processed_currency), + ) { + ( + Some(requested_amount), + Some(requested_currency), + Some(processed_amount), + Some(processed_currency), + ) => Some(NuveiPartialApproval { + requested_amount, + requested_currency, + processed_amount, + processed_currency, + }), + _ => None, + }, + None => None, + } + } +} + +pub fn get_amount_captured( + partial_approval_data: Option, + transaction_type: Option, +) -> Result<(Option, Option), error_stack::Report> { + match partial_approval_data { + Some(partial_approval) => { + let amount = utils::convert_back_amount_to_minor_units( + NUVEI_AMOUNT_CONVERTOR, + partial_approval.processed_amount.clone(), + partial_approval.processed_currency, + )?; + match transaction_type { + None => Ok((None, None)), + Some(NuveiTransactionType::Sale) => { + Ok((Some(MinorUnit::get_amount_as_i64(amount)), None)) + } + Some(NuveiTransactionType::Auth) => Ok((None, Some(amount))), + Some(NuveiTransactionType::Auth3D) => { + Ok((Some(MinorUnit::get_amount_as_i64(amount)), None)) } + Some(NuveiTransactionType::InitAuth3D) => Ok((None, Some(amount))), + Some(NuveiTransactionType::Credit) => Ok((None, None)), + Some(NuveiTransactionType::Void) => Ok((None, None)), + Some(NuveiTransactionType::Settle) => Ok((None, None)), } - None => Ok((None, None)), } + None => Ok((None, None)), } } @@ -2301,13 +2386,15 @@ pub struct FraudDetails { } fn get_payment_status( - response: &NuveiPaymentsResponse, amount: Option, is_post_capture_void: bool, + transaction_type: Option, + transaction_status: Option, + status: NuveiPaymentStatus, ) -> enums::AttemptStatus { // ZERO dollar authorization - if amount == Some(0) && response.transaction_type.clone() == Some(NuveiTransactionType::Auth) { - return match response.transaction_status.clone() { + if amount == Some(0) && transaction_type == Some(NuveiTransactionType::Auth) { + return match transaction_status { Some(NuveiTransactionStatus::Approved) => enums::AttemptStatus::Charged, Some(NuveiTransactionStatus::Declined) | Some(NuveiTransactionStatus::Error) => { enums::AttemptStatus::AuthorizationFailed @@ -2316,7 +2403,7 @@ fn get_payment_status( enums::AttemptStatus::Pending } Some(NuveiTransactionStatus::Redirect) => enums::AttemptStatus::AuthenticationPending, - None => match response.status { + None => match status { NuveiPaymentStatus::Failed | NuveiPaymentStatus::Error => { enums::AttemptStatus::Failure } @@ -2325,10 +2412,12 @@ fn get_payment_status( }; } - match response.transaction_status.clone() { + match transaction_status { Some(status) => match status { - NuveiTransactionStatus::Approved => match response.transaction_type { - Some(NuveiTransactionType::Auth) => enums::AttemptStatus::Authorized, + NuveiTransactionStatus::Approved => match transaction_type { + Some(NuveiTransactionType::InitAuth3D) | Some(NuveiTransactionType::Auth) => { + enums::AttemptStatus::Authorized + } Some(NuveiTransactionType::Sale) | Some(NuveiTransactionType::Settle) => { enums::AttemptStatus::Charged } @@ -2336,14 +2425,14 @@ fn get_payment_status( enums::AttemptStatus::VoidedPostCharge } Some(NuveiTransactionType::Void) => enums::AttemptStatus::Voided, - + Some(NuveiTransactionType::Auth3D) => enums::AttemptStatus::AuthenticationPending, _ => enums::AttemptStatus::Pending, }, NuveiTransactionStatus::Declined | NuveiTransactionStatus::Error => { - match response.transaction_type { + match transaction_type { Some(NuveiTransactionType::Auth) => enums::AttemptStatus::AuthorizationFailed, Some(NuveiTransactionType::Void) => enums::AttemptStatus::VoidFailed, - Some(NuveiTransactionType::Auth3D) => { + Some(NuveiTransactionType::Auth3D) | Some(NuveiTransactionType::InitAuth3D) => { enums::AttemptStatus::AuthenticationFailed } _ => enums::AttemptStatus::Failure, @@ -2354,37 +2443,49 @@ fn get_payment_status( } NuveiTransactionStatus::Redirect => enums::AttemptStatus::AuthenticationPending, }, - None => match response.status { + None => match status { NuveiPaymentStatus::Failed | NuveiPaymentStatus::Error => enums::AttemptStatus::Failure, _ => enums::AttemptStatus::Pending, }, } } -fn build_error_response(response: &NuveiPaymentsResponse, http_code: u16) -> Option { - match response.status { +#[derive(Debug)] +struct ErrorResponseParams { + http_code: u16, + status: NuveiPaymentStatus, + err_code: Option, + err_msg: Option, + merchant_advice_code: Option, + gw_error_code: Option, + gw_error_reason: Option, + transaction_status: Option, +} + +fn build_error_response(params: ErrorResponseParams) -> Option { + match params.status { NuveiPaymentStatus::Error => Some(get_error_response( - response.err_code, - &response.reason, - http_code, - &response.merchant_advice_code, - &response.gw_error_code.map(|e| e.to_string()), - &response.gw_error_reason, + params.err_code, + params.err_msg.clone(), + params.http_code, + params.merchant_advice_code.clone(), + params.gw_error_code.map(|code| code.to_string()), + params.gw_error_reason.clone(), )), _ => { let err = Some(get_error_response( - response.gw_error_code, - &response.gw_error_reason, - http_code, - &response.merchant_advice_code, - &response.gw_error_code.map(|e| e.to_string()), - &response.gw_error_reason, + params.gw_error_code, + params.gw_error_reason.clone(), + params.http_code, + params.merchant_advice_code, + params.gw_error_code.map(|e| e.to_string()), + params.gw_error_reason.clone(), )); - match response.transaction_status { + match params.transaction_status { Some(NuveiTransactionStatus::Error) | Some(NuveiTransactionStatus::Declined) => err, - _ => match response + _ => match params .gw_error_reason .as_ref() .map(|r| r.eq("Missing argument")) @@ -2433,11 +2534,15 @@ impl >, ) -> Result { let amount = item.data.request.amount; + let response = &item.response; + let (status, redirection_data, connector_response_data) = process_nuvei_payment_response( + NuveiPaymentResponseData::new(amount, false, item.data.payment_method, response), + )?; - let (status, redirection_data, connector_response_data) = - process_nuvei_payment_response(&item, amount, false)?; - - let (amount_captured, minor_amount_capturable) = item.response.get_amount_captured()?; + let (amount_captured, minor_amount_capturable) = get_amount_captured( + response.partial_approval.clone(), + response.transaction_type.clone(), + )?; let ip_address = item .data @@ -2453,16 +2558,31 @@ impl field_name: "browser_info.ip_address", })? .to_string(); + let response = &item.response; Ok(Self { status, - response: if let Some(err) = build_error_response(&item.response, item.http_code) { + response: if let Some(err) = build_error_response(ErrorResponseParams { + http_code: item.http_code, + status: response.status.clone(), + err_code: response.err_code, + err_msg: response.reason.clone(), + merchant_advice_code: response.merchant_advice_code.clone(), + gw_error_code: response.gw_error_code, + gw_error_reason: response.gw_error_reason.clone(), + transaction_status: response.transaction_status.clone(), + }) { Err(err) } else { + let response = &item.response; Ok(create_transaction_response( - &item.response, redirection_data, Some(ip_address), + response.transaction_id.clone(), + response.order_id.clone(), + response.session_token.clone(), + response.external_scheme_transaction_id.clone(), + response.payment_option.clone(), )?) }, amount_captured, @@ -2475,10 +2595,64 @@ impl // Helper function to process Nuvei payment response -fn process_nuvei_payment_response( - item: &ResponseRouterData, - amount: Option, - is_post_capture_void: bool, +/// Struct to encapsulate parameters for processing Nuvei payment responses +#[derive(Debug)] +pub struct NuveiPaymentResponseData { + pub amount: Option, + pub is_post_capture_void: bool, + pub payment_method: enums::PaymentMethod, + pub payment_option: Option, + pub transaction_type: Option, + pub transaction_status: Option, + pub status: NuveiPaymentStatus, + pub merchant_advice_code: Option, +} + +impl NuveiPaymentResponseData { + pub fn new( + amount: Option, + is_post_capture_void: bool, + payment_method: enums::PaymentMethod, + response: &NuveiPaymentsResponse, + ) -> Self { + Self { + amount, + is_post_capture_void, + payment_method, + payment_option: response.payment_option.clone(), + transaction_type: response.transaction_type.clone(), + transaction_status: response.transaction_status.clone(), + status: response.status.clone(), + merchant_advice_code: response.merchant_advice_code.clone(), + } + } + + pub fn new_from_sync_response( + amount: Option, + is_post_capture_void: bool, + payment_method: enums::PaymentMethod, + response: &NuveiTransactionSyncResponse, + ) -> Self { + let transaction_details = &response.transaction_details; + Self { + amount, + is_post_capture_void, + payment_method, + payment_option: response.payment_option.clone(), + transaction_type: transaction_details + .as_ref() + .and_then(|details| details.transaction_type.clone()), + transaction_status: transaction_details + .as_ref() + .and_then(|details| details.transaction_status.clone()), + status: response.status.clone(), + merchant_advice_code: None, + } + } +} + +fn process_nuvei_payment_response( + data: NuveiPaymentResponseData, ) -> Result< ( enums::AttemptStatus, @@ -2486,20 +2660,14 @@ fn process_nuvei_payment_response( Option, ), error_stack::Report, -> -where - F: std::fmt::Debug, - T: std::fmt::Debug, -{ - let redirection_data = match item.data.payment_method { - enums::PaymentMethod::Wallet | enums::PaymentMethod::BankRedirect => item - .response +> { + let redirection_data = match data.payment_method { + enums::PaymentMethod::Wallet | enums::PaymentMethod::BankRedirect => data .payment_option .as_ref() .and_then(|po| po.redirect_url.clone()) .map(|base_url| RedirectForm::from((base_url, Method::Get))), - _ => item - .response + _ => data .payment_option .as_ref() .and_then(|o| o.card.clone()) @@ -2511,32 +2679,42 @@ where form_fields: std::collections::HashMap::from([("creq".to_string(), creq.expose())]), }), }; - let connector_response_data = - convert_to_additional_payment_method_connector_response(&item.response) - .map(ConnectorResponseData::with_additional_payment_method_data); - let status = get_payment_status(&item.response, amount, is_post_capture_void); + let connector_response_data = convert_to_additional_payment_method_connector_response( + data.payment_option.clone(), + data.merchant_advice_code, + ) + .map(ConnectorResponseData::with_additional_payment_method_data); + let status = get_payment_status( + data.amount, + data.is_post_capture_void, + data.transaction_type, + data.transaction_status, + data.status, + ); Ok((status, redirection_data, connector_response_data)) } // Helper function to create transaction response fn create_transaction_response( - response: &NuveiPaymentsResponse, redirection_data: Option, ip_address: Option, + transaction_id: Option, + order_id: Option, + session_token: Option>, + external_scheme_transaction_id: Option>, + payment_option: Option, ) -> Result> { Ok(PaymentsResponseData::TransactionResponse { - resource_id: response - .transaction_id + resource_id: transaction_id .clone() - .map_or(response.order_id.clone(), Some) // For paypal there will be no transaction_id, only order_id will be present + .map_or(order_id.clone(), Some) // For paypal there will be no transaction_id, only order_id will be present .map(ResponseId::ConnectorTransactionId) .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?, redirection_data: Box::new(redirection_data), mandate_reference: Box::new( - response - .payment_option + payment_option .as_ref() .and_then(|po| po.user_payment_option_id.clone()) .map(|id| MandateReference { @@ -2548,7 +2726,7 @@ fn create_transaction_response( }), ), // we don't need to save session token for capture, void flow so ignoring if it is not present - connector_metadata: if let Some(token) = response.session_token.clone() { + connector_metadata: if let Some(token) = session_token { Some( serde_json::to_value(NuveiMeta { session_token: token, @@ -2558,11 +2736,10 @@ fn create_transaction_response( } else { None }, - network_txn_id: response - .external_scheme_transaction_id + network_txn_id: external_scheme_transaction_id .as_ref() .map(|ntid| ntid.clone().expose()), - connector_response_reference_id: response.order_id.clone(), + connector_response_reference_id: order_id.clone(), incremental_authorization_allowed: None, charges: None, }) @@ -2590,11 +2767,15 @@ impl ) -> Result { // Get amount directly from the authorize data let amount = Some(item.data.request.amount); + let response = &item.response; + let (status, redirection_data, connector_response_data) = process_nuvei_payment_response( + NuveiPaymentResponseData::new(amount, false, item.data.payment_method, response), + )?; - let (status, redirection_data, connector_response_data) = - process_nuvei_payment_response(&item, amount, false)?; - - let (amount_captured, minor_amount_capturable) = item.response.get_amount_captured()?; + let (amount_captured, minor_amount_capturable) = get_amount_captured( + response.partial_approval.clone(), + response.transaction_type.clone(), + )?; let ip_address = item .data @@ -2605,13 +2786,27 @@ impl Ok(Self { status, - response: if let Some(err) = build_error_response(&item.response, item.http_code) { + response: if let Some(err) = build_error_response(ErrorResponseParams { + http_code: item.http_code, + status: response.status.clone(), + err_code: response.err_code, + err_msg: response.reason.clone(), + merchant_advice_code: response.merchant_advice_code.clone(), + gw_error_code: response.gw_error_code, + gw_error_reason: response.gw_error_reason.clone(), + transaction_status: response.transaction_status.clone(), + }) { Err(err) } else { + let response = &item.response; Ok(create_transaction_response( - &item.response, redirection_data, ip_address, + response.transaction_id.clone(), + response.order_id.clone(), + response.session_token.clone(), + response.external_scheme_transaction_id.clone(), + response.payment_option.clone(), )?) }, amount_captured, @@ -2638,19 +2833,113 @@ where .data .minor_amount_capturable .map(|amount| amount.get_amount_as_i64()); + let response = &item.response; let (status, redirection_data, connector_response_data) = - process_nuvei_payment_response(&item, amount, F::is_post_capture_void())?; + process_nuvei_payment_response(NuveiPaymentResponseData::new( + amount, + F::is_post_capture_void(), + item.data.payment_method, + response, + ))?; + + let (amount_captured, minor_amount_capturable) = get_amount_captured( + response.partial_approval.clone(), + response.transaction_type.clone(), + )?; + Ok(Self { + status, + response: if let Some(err) = build_error_response(ErrorResponseParams { + http_code: item.http_code, + status: response.status.clone(), + err_code: response.err_code, + err_msg: response.reason.clone(), + merchant_advice_code: response.merchant_advice_code.clone(), + gw_error_code: response.gw_error_code, + gw_error_reason: response.gw_error_reason.clone(), + transaction_status: response.transaction_status.clone(), + }) { + Err(err) + } else { + let response = &item.response; + Ok(create_transaction_response( + redirection_data, + None, + response.transaction_id.clone(), + response.order_id.clone(), + response.session_token.clone(), + response.external_scheme_transaction_id.clone(), + response.payment_option.clone(), + )?) + }, + amount_captured, + minor_amount_capturable, + connector_response: connector_response_data, + ..item.data + }) + } +} - let (amount_captured, minor_amount_capturable) = item.response.get_amount_captured()?; +// Generic implementation for other flow types +impl TryFrom> + for RouterData +where + F: NuveiPaymentsGenericResponse + std::fmt::Debug, + T: std::fmt::Debug, + F: std::any::Any, +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let amount = item + .data + .minor_amount_capturable + .map(|amount| amount.get_amount_as_i64()); + let response = &item.response; + let transaction_details = &response.transaction_details; + let transaction_type = transaction_details + .as_ref() + .and_then(|details| details.transaction_type.clone()); + let (status, redirection_data, connector_response_data) = + process_nuvei_payment_response(NuveiPaymentResponseData::new_from_sync_response( + amount, + F::is_post_capture_void(), + item.data.payment_method, + response, + ))?; + + let (amount_captured, minor_amount_capturable) = + get_amount_captured(response.get_partial_approval(), transaction_type.clone())?; Ok(Self { status, - response: if let Some(err) = build_error_response(&item.response, item.http_code) { + response: if let Some(err) = build_error_response(ErrorResponseParams { + http_code: item.http_code, + status: response.status.clone(), + err_code: response.err_code, + err_msg: response.reason.clone(), + merchant_advice_code: None, + gw_error_code: transaction_details + .as_ref() + .and_then(|details| details.gw_error_code), + gw_error_reason: transaction_details + .as_ref() + .and_then(|details| details.gw_error_reason.clone()), + transaction_status: transaction_details + .as_ref() + .and_then(|details| details.transaction_status.clone()), + }) { Err(err) } else { Ok(create_transaction_response( - &item.response, redirection_data, None, + transaction_details + .as_ref() + .and_then(|data| data.transaction_id.clone()), + None, + None, + None, + response.payment_option.clone(), )?) }, amount_captured, @@ -2678,12 +2967,17 @@ impl TryFrom> .map(to_boolean) .unwrap_or_default(); Ok(Self { - status: get_payment_status(&response, item.data.request.amount, false), + status: get_payment_status( + item.data.request.amount, + false, + response.transaction_type, + response.transaction_status, + response.status, + ), response: Ok(PaymentsResponseData::ThreeDSEnrollmentResponse { enrolled_v2: is_enrolled_for_3ds, related_transaction_id: response.transaction_id, }), - ..item.data }) } @@ -2760,11 +3054,7 @@ where .request .get_customer_id_required() .ok_or(missing_field_err("customer_id")())?; - let related_transaction_id = if item.is_three_ds() { - item.request.get_related_transaction_id().clone() - } else { - None - }; + let related_transaction_id = item.request.get_related_transaction_id().clone(); let ip_address = data .recurring_mandate_payment_data @@ -2823,20 +3113,20 @@ fn get_refund_response( match response.status { NuveiPaymentStatus::Error => Err(Box::new(get_error_response( response.err_code, - &response.reason, + response.reason.clone(), http_code, - &response.merchant_advice_code, - &response.gw_error_code.map(|e| e.to_string()), - &response.gw_error_reason, + response.merchant_advice_code, + response.gw_error_code.map(|e| e.to_string()), + response.gw_error_reason, ))), _ => match response.transaction_status { Some(NuveiTransactionStatus::Error) => Err(Box::new(get_error_response( response.err_code, - &response.reason, + response.reason, http_code, - &response.merchant_advice_code, - &response.gw_error_code.map(|e| e.to_string()), - &response.gw_error_reason, + response.merchant_advice_code, + response.gw_error_code.map(|e| e.to_string()), + response.gw_error_reason, ))), _ => Ok(RefundsResponseData { connector_refund_id: txn_id, @@ -2848,11 +3138,11 @@ fn get_refund_response( fn get_error_response( error_code: Option, - error_msg: &Option, + error_msg: Option, http_code: u16, - network_advice_code: &Option, - network_decline_code: &Option, - network_error_message: &Option, + network_advice_code: Option, + network_decline_code: Option, + network_error_message: Option, ) -> ErrorResponse { ErrorResponse { code: error_code @@ -3049,6 +3339,7 @@ impl From for NuveiPaymentsResponse { TransactionStatus::Declined => NuveiTransactionStatus::Declined, TransactionStatus::Error => NuveiTransactionStatus::Error, TransactionStatus::Settled => NuveiTransactionStatus::Approved, + _ => NuveiTransactionStatus::Processing, }), transaction_id: notification.transaction_id, @@ -3116,21 +3407,17 @@ pub fn concat_strings(strings: &[String]) -> String { } fn convert_to_additional_payment_method_connector_response( - transaction_response: &NuveiPaymentsResponse, + payment_option: Option, + merchant_advice_code: Option, ) -> Option { - let card = transaction_response - .payment_option - .as_ref()? - .card - .as_ref()?; + let card = payment_option.as_ref()?.card.as_ref()?; let avs_code = card.avs_code.as_ref(); let cvv2_code = card.cvv2_reply.as_ref(); - let merchant_advice_code = transaction_response.merchant_advice_code.as_ref(); - let avs_description = avs_code.and_then(|code| get_avs_response_description(code)); let cvv_description = cvv2_code.and_then(|code| get_cvv2_response_description(code)); - let merchant_advice_description = - merchant_advice_code.and_then(|code| get_merchant_advice_code_description(code)); + let merchant_advice_description = merchant_advice_code + .as_ref() + .and_then(|code| get_merchant_advice_code_description(code)); let payment_checks = serde_json::json!({ "avs_result": avs_code, diff --git a/cypress-tests/cypress/e2e/configs/Payment/Nuvei.js b/cypress-tests/cypress/e2e/configs/Payment/Nuvei.js index 008588194eb..2761cc2c00a 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Nuvei.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Nuvei.js @@ -38,7 +38,7 @@ const multiUseMandateData = { // Payment method data objects for responses const payment_method_data_no3ds = { card: { - authentication_data: {}, + authentication_data: { challengePreferenceReason: "12" }, last4: "1111", card_type: "CREDIT", card_network: "Visa",