Skip to content

Commit a132cb5

Browse files
feat(connector): [NUVEI] Added support for AVC CVV checks, post confirm void and 0$ txns (#8766)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 9de83ee commit a132cb5

File tree

4 files changed

+882
-238
lines changed

4 files changed

+882
-238
lines changed

crates/hyperswitch_connectors/src/connectors/nuvei.rs

Lines changed: 178 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,23 @@ use hyperswitch_domain_models::{
1818
access_token_auth::AccessTokenAuth,
1919
payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void},
2020
refunds::{Execute, RSync},
21-
AuthorizeSessionToken, CompleteAuthorize, PreProcessing,
21+
AuthorizeSessionToken, CompleteAuthorize, PostCaptureVoid, PreProcessing,
2222
},
2323
router_request_types::{
2424
AccessTokenRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData,
2525
PaymentMethodTokenizationData, PaymentsAuthorizeData, PaymentsCancelData,
26-
PaymentsCaptureData, PaymentsPreProcessingData, PaymentsSessionData, PaymentsSyncData,
27-
RefundsData, SetupMandateRequestData,
26+
PaymentsCancelPostCaptureData, PaymentsCaptureData, PaymentsPreProcessingData,
27+
PaymentsSessionData, PaymentsSyncData, RefundsData, SetupMandateRequestData,
2828
},
2929
router_response_types::{
3030
ConnectorInfo, PaymentMethodDetails, PaymentsResponseData, RefundsResponseData,
3131
SupportedPaymentMethods, SupportedPaymentMethodsExt,
3232
},
3333
types::{
3434
PaymentsAuthorizeRouterData, PaymentsAuthorizeSessionTokenRouterData,
35-
PaymentsCancelRouterData, PaymentsCaptureRouterData, PaymentsCompleteAuthorizeRouterData,
36-
PaymentsPreProcessingRouterData, PaymentsSyncRouterData, RefundsRouterData,
35+
PaymentsCancelPostCaptureRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
36+
PaymentsCompleteAuthorizeRouterData, PaymentsPreProcessingRouterData,
37+
PaymentsSyncRouterData, RefundsRouterData,
3738
},
3839
};
3940
use hyperswitch_interfaces::{
@@ -131,7 +132,7 @@ impl api::RefundSync for Nuvei {}
131132
impl api::PaymentsCompleteAuthorize for Nuvei {}
132133
impl api::ConnectorAccessToken for Nuvei {}
133134
impl api::PaymentsPreProcessing for Nuvei {}
134-
135+
impl api::PaymentPostCaptureVoid for Nuvei {}
135136
impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData> for Nuvei {
136137
fn build_request(
137138
&self,
@@ -175,7 +176,6 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
175176
) -> CustomResult<RequestContent, errors::ConnectorError> {
176177
let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?;
177178
let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?;
178-
179179
Ok(RequestContent::Json(Box::new(connector_req)))
180180
}
181181
fn build_request(
@@ -209,7 +209,6 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
209209
.response
210210
.parse_struct("NuveiPaymentsResponse")
211211
.switch()?;
212-
213212
event_builder.map(|i| i.set_response_body(&response));
214213
router_env::logger::info!(connector_response=?response);
215214

@@ -309,6 +308,90 @@ impl ConnectorIntegration<Void, PaymentsCancelData, PaymentsResponseData> for Nu
309308
}
310309
}
311310

311+
impl ConnectorIntegration<PostCaptureVoid, PaymentsCancelPostCaptureData, PaymentsResponseData>
312+
for Nuvei
313+
{
314+
fn get_headers(
315+
&self,
316+
req: &PaymentsCancelPostCaptureRouterData,
317+
connectors: &Connectors,
318+
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
319+
self.build_headers(req, connectors)
320+
}
321+
322+
fn get_content_type(&self) -> &'static str {
323+
self.common_get_content_type()
324+
}
325+
326+
fn get_url(
327+
&self,
328+
_req: &PaymentsCancelPostCaptureRouterData,
329+
connectors: &Connectors,
330+
) -> CustomResult<String, errors::ConnectorError> {
331+
Ok(format!(
332+
"{}ppp/api/v1/voidTransaction.do",
333+
ConnectorCommon::base_url(self, connectors)
334+
))
335+
}
336+
337+
fn get_request_body(
338+
&self,
339+
req: &PaymentsCancelPostCaptureRouterData,
340+
_connectors: &Connectors,
341+
) -> CustomResult<RequestContent, errors::ConnectorError> {
342+
let connector_req = nuvei::NuveiVoidRequest::try_from(req)?;
343+
Ok(RequestContent::Json(Box::new(connector_req)))
344+
}
345+
346+
fn build_request(
347+
&self,
348+
req: &PaymentsCancelPostCaptureRouterData,
349+
connectors: &Connectors,
350+
) -> CustomResult<Option<Request>, errors::ConnectorError> {
351+
let request = RequestBuilder::new()
352+
.method(Method::Post)
353+
.url(&types::PaymentsPostCaptureVoidType::get_url(
354+
self, req, connectors,
355+
)?)
356+
.attach_default_headers()
357+
.headers(types::PaymentsPostCaptureVoidType::get_headers(
358+
self, req, connectors,
359+
)?)
360+
.set_body(types::PaymentsPostCaptureVoidType::get_request_body(
361+
self, req, connectors,
362+
)?)
363+
.build();
364+
Ok(Some(request))
365+
}
366+
367+
fn handle_response(
368+
&self,
369+
data: &PaymentsCancelPostCaptureRouterData,
370+
event_builder: Option<&mut ConnectorEvent>,
371+
res: Response,
372+
) -> CustomResult<PaymentsCancelPostCaptureRouterData, errors::ConnectorError> {
373+
let response: nuvei::NuveiPaymentsResponse = res
374+
.response
375+
.parse_struct("NuveiPaymentsResponse")
376+
.switch()?;
377+
event_builder.map(|i| i.set_response_body(&response));
378+
router_env::logger::info!(connector_response=?response);
379+
RouterData::try_from(ResponseRouterData {
380+
response,
381+
data: data.clone(),
382+
http_code: res.status_code,
383+
})
384+
.change_context(errors::ConnectorError::ResponseHandlingFailed)
385+
}
386+
387+
fn get_error_response(
388+
&self,
389+
res: Response,
390+
event_builder: Option<&mut ConnectorEvent>,
391+
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
392+
self.build_error_response(res, event_builder)
393+
}
394+
}
312395
impl ConnectorIntegration<AccessTokenAuth, AccessTokenRequestData, AccessToken> for Nuvei {}
313396

314397
impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Nuvei {
@@ -509,7 +592,6 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
509592
_connectors: &Connectors,
510593
) -> CustomResult<RequestContent, errors::ConnectorError> {
511594
let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?;
512-
513595
Ok(RequestContent::Json(Box::new(connector_req)))
514596
}
515597

@@ -545,7 +627,6 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
545627
.response
546628
.parse_struct("NuveiPaymentsResponse")
547629
.switch()?;
548-
549630
event_builder.map(|i| i.set_response_body(&response));
550631
router_env::logger::info!(connector_response=?response);
551632

@@ -683,7 +764,6 @@ impl ConnectorIntegration<PreProcessing, PaymentsPreProcessingData, PaymentsResp
683764
_connectors: &Connectors,
684765
) -> CustomResult<RequestContent, errors::ConnectorError> {
685766
let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?;
686-
687767
Ok(RequestContent::Json(Box::new(connector_req)))
688768
}
689769

@@ -719,7 +799,6 @@ impl ConnectorIntegration<PreProcessing, PaymentsPreProcessingData, PaymentsResp
719799
.response
720800
.parse_struct("NuveiPaymentsResponse")
721801
.switch()?;
722-
723802
event_builder.map(|i| i.set_response_body(&response));
724803
router_env::logger::info!(connector_response=?response);
725804

@@ -802,7 +881,6 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Nuvei {
802881
.response
803882
.parse_struct("NuveiPaymentsResponse")
804883
.switch()?;
805-
806884
event_builder.map(|i| i.set_response_body(&response));
807885
router_env::logger::info!(connector_response=?response);
808886

@@ -849,59 +927,118 @@ impl IncomingWebhook for Nuvei {
849927
_merchant_id: &id_type::MerchantId,
850928
connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
851929
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
852-
let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params)
930+
// Parse the webhook payload
931+
let webhook = serde_urlencoded::from_str::<nuvei::NuveiWebhook>(&request.query_params)
853932
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
933+
854934
let secret_str = std::str::from_utf8(&connector_webhook_secrets.secret)
855935
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
856-
let status = format!("{:?}", body.status).to_uppercase();
857-
let to_sign = format!(
858-
"{}{}{}{}{}{}{}",
859-
secret_str,
860-
body.total_amount,
861-
body.currency,
862-
body.response_time_stamp,
863-
body.ppp_transaction_id,
864-
status,
865-
body.product_id
866-
);
867-
Ok(to_sign.into_bytes())
936+
937+
// Generate signature based on webhook type
938+
match webhook {
939+
nuvei::NuveiWebhook::PaymentDmn(notification) => {
940+
// For payment DMNs, use the same format as before
941+
let status = notification
942+
.transaction_status
943+
.as_ref()
944+
.map(|s| format!("{s:?}").to_uppercase())
945+
.unwrap_or_else(|| "UNKNOWN".to_string());
946+
947+
let to_sign = transformers::concat_strings(&[
948+
secret_str.to_string(),
949+
notification.total_amount.unwrap_or_default(),
950+
notification.currency.unwrap_or_default(),
951+
notification.response_time_stamp.unwrap_or_default(),
952+
notification.ppp_transaction_id.unwrap_or_default(),
953+
status,
954+
notification.product_id.unwrap_or_default(),
955+
]);
956+
Ok(to_sign.into_bytes())
957+
}
958+
nuvei::NuveiWebhook::Chargeback(notification) => {
959+
// For chargeback notifications, use a different format based on Nuvei's documentation
960+
// Note: This is a placeholder - you'll need to adjust based on Nuvei's actual chargeback signature format
961+
let status = notification
962+
.status
963+
.as_ref()
964+
.map(|s| format!("{s:?}").to_uppercase())
965+
.unwrap_or_else(|| "UNKNOWN".to_string());
966+
967+
let to_sign = transformers::concat_strings(&[
968+
secret_str.to_string(),
969+
notification.chargeback_amount.unwrap_or_default(),
970+
notification.chargeback_currency.unwrap_or_default(),
971+
notification.ppp_transaction_id.unwrap_or_default(),
972+
status,
973+
]);
974+
Ok(to_sign.into_bytes())
975+
}
976+
}
868977
}
869978

870979
fn get_webhook_object_reference_id(
871980
&self,
872981
request: &IncomingWebhookRequestDetails<'_>,
873982
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
874-
let body =
875-
serde_urlencoded::from_str::<nuvei::NuveiWebhookTransactionId>(&request.query_params)
876-
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
983+
// Parse the webhook payload
984+
let webhook = serde_urlencoded::from_str::<nuvei::NuveiWebhook>(&request.query_params)
985+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
986+
987+
// Extract transaction ID from the webhook
988+
let transaction_id = match &webhook {
989+
nuvei::NuveiWebhook::PaymentDmn(notification) => {
990+
notification.ppp_transaction_id.clone().unwrap_or_default()
991+
}
992+
nuvei::NuveiWebhook::Chargeback(notification) => {
993+
notification.ppp_transaction_id.clone().unwrap_or_default()
994+
}
995+
};
996+
877997
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
878-
PaymentIdType::ConnectorTransactionId(body.ppp_transaction_id),
998+
PaymentIdType::ConnectorTransactionId(transaction_id),
879999
))
8801000
}
8811001

8821002
fn get_webhook_event_type(
8831003
&self,
8841004
request: &IncomingWebhookRequestDetails<'_>,
8851005
) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
886-
let body =
887-
serde_urlencoded::from_str::<nuvei::NuveiWebhookDataStatus>(&request.query_params)
888-
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
889-
match body.status {
890-
nuvei::NuveiWebhookStatus::Approved => Ok(IncomingWebhookEvent::PaymentIntentSuccess),
891-
nuvei::NuveiWebhookStatus::Declined => Ok(IncomingWebhookEvent::PaymentIntentFailure),
892-
nuvei::NuveiWebhookStatus::Unknown
893-
| nuvei::NuveiWebhookStatus::Pending
894-
| nuvei::NuveiWebhookStatus::Update => Ok(IncomingWebhookEvent::EventNotSupported),
1006+
// Parse the webhook payload
1007+
let webhook = serde_urlencoded::from_str::<nuvei::NuveiWebhook>(&request.query_params)
1008+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
1009+
1010+
// Map webhook type to event type
1011+
match webhook {
1012+
nuvei::NuveiWebhook::PaymentDmn(notification) => {
1013+
match notification.transaction_status {
1014+
Some(nuvei::TransactionStatus::Approved)
1015+
| Some(nuvei::TransactionStatus::Settled) => {
1016+
Ok(IncomingWebhookEvent::PaymentIntentSuccess)
1017+
}
1018+
Some(nuvei::TransactionStatus::Declined)
1019+
| Some(nuvei::TransactionStatus::Error) => {
1020+
Ok(IncomingWebhookEvent::PaymentIntentFailure)
1021+
}
1022+
_ => Ok(IncomingWebhookEvent::EventNotSupported),
1023+
}
1024+
}
1025+
nuvei::NuveiWebhook::Chargeback(_) => {
1026+
// Chargeback notifications always map to dispute opened
1027+
Ok(IncomingWebhookEvent::DisputeOpened)
1028+
}
8951029
}
8961030
}
8971031

8981032
fn get_webhook_resource_object(
8991033
&self,
9001034
request: &IncomingWebhookRequestDetails<'_>,
9011035
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
902-
let body = serde_urlencoded::from_str::<nuvei::NuveiWebhookDetails>(&request.query_params)
1036+
// Parse the webhook payload
1037+
let webhook = serde_urlencoded::from_str::<nuvei::NuveiWebhook>(&request.query_params)
9031038
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
904-
let payment_response = nuvei::NuveiPaymentsResponse::from(body);
1039+
1040+
// Convert webhook to payments response
1041+
let payment_response = nuvei::NuveiPaymentsResponse::from(webhook);
9051042

9061043
Ok(Box::new(payment_response))
9071044
}

0 commit comments

Comments
 (0)