fix(nuvei): nuvei 3ds fix + psync fix (#9279)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Nithin N
2025-09-11 19:09:31 +05:30
committed by GitHub
parent cadfcf7c22
commit ebba12ecd5
3 changed files with 545 additions and 262 deletions

View File

@ -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<SetupMandate, SetupMandateRequestData, PaymentsRespons
RouterData<SetupMandate, SetupMandateRequestData, PaymentsResponseData>,
errors::ConnectorError,
> {
let response: nuvei::NuveiPaymentsResponse = res
let response: NuveiPaymentsResponse = res
.response
.parse_struct("NuveiPaymentsResponse")
.switch()?;
@ -234,6 +235,85 @@ impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsRespons
}
}
impl ConnectorIntegration<Void, PaymentsCancelData, PaymentsResponseData> for Nuvei {
fn get_headers(
&self,
req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}ppp/api/v1/voidTransaction.do",
ConnectorCommon::base_url(self, connectors)
))
}
fn get_request_body(
&self,
req: &PaymentsCancelRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, 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))
}
fn handle_response(
&self,
data: &PaymentsCancelRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsCancelRouterData, errors::ConnectorError> {
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(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResponseData>
for Nuvei
{
@ -293,87 +373,7 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
let response: nuvei::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(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<Void, PaymentsCancelData, PaymentsResponseData> for Nuvei {
fn get_headers(
&self,
req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}ppp/api/v1/voidTransaction.do",
ConnectorCommon::base_url(self, connectors)
))
}
fn get_request_body(
&self,
req: &PaymentsCancelRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &PaymentsCancelRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, 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))
}
fn handle_response(
&self,
data: &PaymentsCancelRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsCancelRouterData, errors::ConnectorError> {
let response: nuvei::NuveiPaymentsResponse = res
let response: NuveiPaymentsResponse = res
.response
.parse_struct("NuveiPaymentsResponse")
.switch()?;
@ -458,7 +458,7 @@ impl ConnectorIntegration<PostCaptureVoid, PaymentsCancelPostCaptureData, Paymen
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsCancelPostCaptureRouterData, errors::ConnectorError> {
let response: nuvei::NuveiPaymentsResponse = res
let response: NuveiPaymentsResponse = res
.response
.parse_struct("NuveiPaymentsResponse")
.switch()?;
@ -501,7 +501,7 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Nuv
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}ppp/api/v1/getPaymentStatus.do",
"{}ppp/api/v1/getTransactionDetails.do",
ConnectorCommon::base_url(self, connectors)
))
}
@ -546,9 +546,9 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Nuv
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsSyncRouterData, errors::ConnectorError> {
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<Capture, PaymentsCaptureData, PaymentsResponseData> fo
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsCaptureRouterData, errors::ConnectorError> {
let response: nuvei::NuveiPaymentsResponse = res
let response: NuveiPaymentsResponse = res
.response
.parse_struct("NuveiPaymentsResponse")
.switch()?;
@ -678,7 +678,6 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
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<Authorize, PaymentsAuthorizeData, PaymentsResponseData
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: nuvei::NuveiPaymentsResponse = res
let response: NuveiPaymentsResponse = res
.response
.parse_struct("NuveiPaymentsResponse")
.switch()?;
@ -883,13 +882,12 @@ impl ConnectorIntegration<PreProcessing, PaymentsPreProcessingData, PaymentsResp
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsPreProcessingRouterData, errors::ConnectorError> {
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<Execute, RefundsData, RefundsResponseData> for Nuvei {
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<RefundsRouterData<Execute>, 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::<nuvei::NuveiWebhook>(&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))
}
}

View File

@ -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<String, error_stack::Report<errors::ConnectorError>> {
self.router_return_url
.clone()
.ok_or_else(missing_field_err("return_url"))
self.get_router_return_url()
}
fn get_capture_method(&self) -> Option<CaptureMethod> {
@ -201,10 +200,6 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsAuthorizeData {
self.customer_id.clone()
}
fn get_complete_authorize_url(&self) -> Option<String> {
self.complete_authorize_url.clone()
}
fn get_connector_mandate_id(&self) -> Option<String> {
self.connector_mandate_id().clone()
}
@ -219,6 +214,10 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsAuthorizeData {
self.capture_method
}
fn get_complete_authorize_url(&self) -> Option<String> {
self.complete_authorize_url.clone()
}
fn get_minor_amount_required(
&self,
) -> Result<MinorUnit, error_stack::Report<errors::ConnectorError>> {
@ -273,10 +272,6 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsPreProcessingData {
&& self.setup_future_usage == Some(FutureUsage::OffSession)
}
fn get_complete_authorize_url(&self) -> Option<String> {
self.complete_authorize_url.clone()
}
fn get_connector_mandate_id(&self) -> Option<String> {
self.connector_mandate_id()
}
@ -291,6 +286,10 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsPreProcessingData {
self.capture_method
}
fn get_complete_authorize_url(&self) -> Option<String> {
self.complete_authorize_url.clone()
}
fn get_minor_amount_required(
&self,
) -> Result<MinorUnit, error_stack::Report<errors::ConnectorError>> {
@ -502,7 +501,11 @@ pub struct NuveiPaymentFlowRequest {
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NuveiPaymentSyncRequest {
pub session_token: Secret<String>,
pub merchant_id: Secret<String>,
pub merchant_site_id: Secret<String>,
pub time_stamp: String,
pub checksum: Secret<String>,
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<ShippingAddress> =
item.get_optional_shipping().map(|address| address.into());
let billing_address: Option<BillingAddress> = address.map(|ref address| address.into());
let billing_address: Option<BillingAddress> =
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<String>)>
..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<String>)>
..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<errors::ConnectorError>;
fn try_from(value: &types::PaymentsSyncRouterData) -> Result<Self, Self::Error> {
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,38 +2266,108 @@ pub struct NuveiPaymentsResponse {
pub client_request_id: Option<String>,
pub merchant_advice_code: Option<String>,
}
impl NuveiPaymentsResponse {
/// returns amount_captured and minor_amount_capturable
pub fn get_amount_captured(
&self,
) -> Result<(Option<i64>, Option<MinorUnit>), error_stack::Report<errors::ConnectorError>> {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NuveiTxnPartialApproval {
requested_amount: Option<StringMajorUnit>,
requested_currency: Option<enums::Currency>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NuveiTransactionSyncResponseDetails {
gw_error_code: Option<i64>,
gw_error_reason: Option<String>,
gw_extended_error_code: Option<i64>,
transaction_id: Option<String>,
transaction_status: Option<NuveiTransactionStatus>,
transaction_type: Option<NuveiTransactionType>,
auth_code: Option<String>,
processed_amount: Option<StringMajorUnit>,
processed_currency: Option<enums::Currency>,
acquiring_bank_name: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NuveiTransactionSyncResponse {
pub payment_option: Option<PaymentOption>,
pub partial_approval: Option<NuveiTxnPartialApproval>,
pub is_currency_converted: Option<bool>,
pub transaction_details: Option<NuveiTransactionSyncResponseDetails>,
pub fraud_details: Option<FraudDetails>,
pub client_unique_id: Option<String>,
pub internal_request_id: Option<i64>,
pub status: NuveiPaymentStatus,
pub err_code: Option<i64>,
pub reason: Option<String>,
pub merchant_id: Option<Secret<String>>,
pub merchant_site_id: Option<Secret<String>>,
pub version: Option<String>,
pub client_request_id: Option<String>,
pub merchant_advice_code: Option<String>,
}
impl NuveiTransactionSyncResponse {
pub fn get_partial_approval(&self) -> Option<NuveiPartialApproval> {
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)),
}
}
None => 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<NuveiPartialApproval>,
transaction_type: Option<NuveiTransactionType>,
) -> Result<(Option<i64>, Option<MinorUnit>), error_stack::Report<errors::ConnectorError>> {
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)),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NuveiTransactionType {
Auth,
@ -2301,13 +2386,15 @@ pub struct FraudDetails {
}
fn get_payment_status(
response: &NuveiPaymentsResponse,
amount: Option<i64>,
is_post_capture_void: bool,
transaction_type: Option<NuveiTransactionType>,
transaction_status: Option<NuveiTransactionStatus>,
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<ErrorResponse> {
match response.status {
#[derive(Debug)]
struct ErrorResponseParams {
http_code: u16,
status: NuveiPaymentStatus,
err_code: Option<i64>,
err_msg: Option<String>,
merchant_advice_code: Option<String>,
gw_error_code: Option<i64>,
gw_error_reason: Option<String>,
transaction_status: Option<NuveiTransactionStatus>,
}
fn build_error_response(params: ErrorResponseParams) -> Option<ErrorResponse> {
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<Self, Self::Error> {
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<F, T>(
item: &ResponseRouterData<F, NuveiPaymentsResponse, T, PaymentsResponseData>,
amount: Option<i64>,
is_post_capture_void: bool,
/// Struct to encapsulate parameters for processing Nuvei payment responses
#[derive(Debug)]
pub struct NuveiPaymentResponseData {
pub amount: Option<i64>,
pub is_post_capture_void: bool,
pub payment_method: enums::PaymentMethod,
pub payment_option: Option<PaymentOption>,
pub transaction_type: Option<NuveiTransactionType>,
pub transaction_status: Option<NuveiTransactionStatus>,
pub status: NuveiPaymentStatus,
pub merchant_advice_code: Option<String>,
}
impl NuveiPaymentResponseData {
pub fn new(
amount: Option<i64>,
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<i64>,
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<F, T>(
Option<ConnectorResponseData>,
),
error_stack::Report<errors::ConnectorError>,
>
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<RedirectForm>,
ip_address: Option<String>,
transaction_id: Option<String>,
order_id: Option<String>,
session_token: Option<Secret<String>>,
external_scheme_transaction_id: Option<Secret<String>>,
payment_option: Option<PaymentOption>,
) -> Result<PaymentsResponseData, error_stack::Report<errors::ConnectorError>> {
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<Self, Self::Error> {
// 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) = item.response.get_amount_captured()?;
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(&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(
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
})
}
}
// Generic implementation for other flow types
impl<F, T> TryFrom<ResponseRouterData<F, NuveiTransactionSyncResponse, T, PaymentsResponseData>>
for RouterData<F, T, PaymentsResponseData>
where
F: NuveiPaymentsGenericResponse + std::fmt::Debug,
T: std::fmt::Debug,
F: std::any::Any,
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: ResponseRouterData<F, NuveiTransactionSyncResponse, T, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
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(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<PaymentsPreprocessingResponseRouterData<NuveiPaymentsResponse>>
.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<i64>,
error_msg: &Option<String>,
error_msg: Option<String>,
http_code: u16,
network_advice_code: &Option<String>,
network_decline_code: &Option<String>,
network_error_message: &Option<String>,
network_advice_code: Option<String>,
network_decline_code: Option<String>,
network_error_message: Option<String>,
) -> ErrorResponse {
ErrorResponse {
code: error_code
@ -3049,6 +3339,7 @@ impl From<NuveiWebhook> 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<PaymentOption>,
merchant_advice_code: Option<String>,
) -> Option<AdditionalPaymentMethodConnectorResponse> {
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,

View File

@ -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",