diff --git a/config/config.example.toml b/config/config.example.toml index 999266321144..4bff8f412c65 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -803,7 +803,7 @@ check_token_status_url= "" # base url to check token status from token servic connector_list = "cybersource" # Supported connectors for network tokenization [network_transaction_id_supported_connectors] -connector_list = "stripe,adyen,cybersource" # Supported connectors for network transaction id +connector_list = "adyen,cybersource,novalnet,stripe,worldpay" # Supported connectors for network transaction id [grpc_client.dynamic_routing_client] # Dynamic Routing Client Configuration host = "localhost" # Client Host diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 6283382258a8..b6e487e5a3b2 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -191,7 +191,7 @@ card.credit = { connector_list = "cybersource" } # Update Mandate sup card.debit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card [network_transaction_id_supported_connectors] -connector_list = "stripe,adyen,cybersource" +connector_list = "adyen,cybersource,novalnet,stripe,worldpay" [payouts] diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index fcfadb339d9d..7242f263672e 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -191,7 +191,7 @@ card.credit = { connector_list = "cybersource" } # Update Mandate sup card.debit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card [network_transaction_id_supported_connectors] -connector_list = "stripe,adyen,cybersource" +connector_list = "adyen,cybersource,novalnet,stripe,worldpay" [payouts] diff --git a/config/development.toml b/config/development.toml index 4c9b8516b5ad..9af3a294d6d9 100644 --- a/config/development.toml +++ b/config/development.toml @@ -654,7 +654,7 @@ card.credit = { connector_list = "cybersource" } card.debit = { connector_list = "cybersource" } [network_transaction_id_supported_connectors] -connector_list = "stripe,adyen,cybersource" +connector_list = "adyen,cybersource,novalnet,stripe,worldpay" [connector_request_reference_id_config] merchant_ids_send_payment_id_as_connector_request_id = [] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 75699d0a9674..28dbdd37c7ab 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -540,7 +540,7 @@ card.credit = { connector_list = "cybersource" } card.debit = { connector_list = "cybersource" } [network_transaction_id_supported_connectors] -connector_list = "stripe,adyen,cybersource" +connector_list = "adyen,cybersource,novalnet,stripe,worldpay" [connector_customer] connector_list = "gocardless,stax,stripe" diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 143cd1aa0474..d685bf153abe 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs @@ -1092,6 +1092,8 @@ pub struct FiuuPaymentSyncResponse { error_desc: String, #[serde(rename = "miscellaneous")] miscellaneous: Option>>, + #[serde(rename = "SchemeTransactionID")] + scheme_transaction_id: Option>, } #[derive(Debug, Serialize, Deserialize, Display, Clone, PartialEq)] @@ -1182,7 +1184,10 @@ impl TryFrom> for PaymentsSy redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, - network_txn_id: None, + network_txn_id: response + .scheme_transaction_id + .as_ref() + .map(|id| id.clone().expose()), connector_response_reference_id: None, incremental_authorization_allowed: None, charge_id: None, diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index 0a70393a13cd..3ce79f5ada6c 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -88,6 +88,13 @@ pub struct NovalnetCard { card_holder: Secret, } +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct NovalnetRawCardDetails { + card_number: CardNumber, + card_expiry_month: Secret, + card_expiry_year: Secret, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NovalnetMandate { token: Secret, @@ -107,6 +114,7 @@ pub struct NovalnetApplePay { #[serde(untagged)] pub enum NovalNetPaymentData { Card(NovalnetCard), + RawCardForNTI(NovalnetRawCardDetails), GooglePay(NovalnetGooglePay), ApplePay(NovalnetApplePay), MandatePayment(NovalnetMandate), @@ -130,6 +138,7 @@ pub struct NovalnetPaymentsRequestTransaction { error_return_url: Option, enforce_3d: Option, //NOTE: Needed for CREDITCARD, GOOGLEPAY create_token: Option, + scheme_tid: Option>, // Card network's transaction ID } #[derive(Debug, Serialize, Clone)] @@ -243,6 +252,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym payment_data: Some(novalnet_card), enforce_3d, create_token, + scheme_tid: None, }; Ok(Self { @@ -274,6 +284,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym payment_data: Some(novalnet_google_pay), enforce_3d, create_token, + scheme_tid: None, }; Ok(Self { @@ -299,6 +310,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym })), enforce_3d: None, create_token, + scheme_tid: None, }; Ok(Self { @@ -340,6 +352,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym payment_data: None, enforce_3d: None, create_token, + scheme_tid: None, }; Ok(Self { merchant, @@ -398,6 +411,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym payment_data: Some(novalnet_mandate_data), enforce_3d, create_token: None, + scheme_tid: None, }; Ok(Self { @@ -407,6 +421,44 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym custom, }) } + Some(api_models::payments::MandateReferenceId::NetworkMandateId( + network_transaction_id, + )) => match item.router_data.request.payment_method_data { + PaymentMethodData::CardDetailsForNetworkTransactionId(ref raw_card_details) => { + let novalnet_card = + NovalNetPaymentData::RawCardForNTI(NovalnetRawCardDetails { + card_number: raw_card_details.card_number.clone(), + card_expiry_month: raw_card_details.card_exp_month.clone(), + card_expiry_year: raw_card_details.card_exp_year.clone(), + }); + + let transaction = NovalnetPaymentsRequestTransaction { + test_mode, + payment_type: NovalNetPaymentTypes::CREDITCARD, + amount: item.amount.clone(), + currency: item.router_data.request.currency, + order_no: item.router_data.connector_request_reference_id.clone(), + hook_url: Some(hook_url), + return_url: Some(return_url.clone()), + error_return_url: Some(return_url.clone()), + payment_data: Some(novalnet_card), + enforce_3d, + create_token, + scheme_tid: Some(network_transaction_id.into()), + }; + + Ok(Self { + merchant, + transaction, + customer, + custom, + }) + } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("novalnet"), + ) + .into()), + }, _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("novalnet"), ) diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay.rs b/crates/hyperswitch_connectors/src/connectors/worldpay.rs index 07e4c178f9b8..104e854e6fee 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay.rs @@ -1199,6 +1199,42 @@ impl IncomingWebhook for Worldpay { let psync_body = WorldpayEventResponse::try_from(body)?; Ok(Box::new(psync_body)) } + + fn get_mandate_details( + &self, + request: &IncomingWebhookRequestDetails<'_>, + ) -> CustomResult< + Option, + errors::ConnectorError, + > { + let body: WorldpayWebhookTransactionId = request + .body + .parse_struct("WorldpayWebhookTransactionId") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + let mandate_reference = body.event_details.token.map(|mandate_token| { + hyperswitch_domain_models::router_flow_types::ConnectorMandateDetails { + connector_mandate_id: mandate_token.href, + } + }); + Ok(mandate_reference) + } + + fn get_network_txn_id( + &self, + request: &IncomingWebhookRequestDetails<'_>, + ) -> CustomResult< + Option, + errors::ConnectorError, + > { + let body: WorldpayWebhookTransactionId = request + .body + .parse_struct("WorldpayWebhookTransactionId") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + let optional_network_txn_id = body.event_details.scheme_reference.map(|network_txn_id| { + hyperswitch_domain_models::router_flow_types::ConnectorNetworkTxnId::new(network_txn_id) + }); + Ok(optional_network_txn_id) + } } impl ConnectorRedirectResponse for Worldpay { diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs index 884caa9e840b..52ae2c4f16f5 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs @@ -52,18 +52,22 @@ pub enum TokenCreationType { Worldpay, } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CustomerAgreement { #[serde(rename = "type")] pub agreement_type: CustomerAgreementType, - pub stored_card_usage: StoredCardUsageType, + pub stored_card_usage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scheme_reference: Option>, } #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum CustomerAgreementType { Subscription, + Unscheduled, } #[derive(Clone, Debug, PartialEq, Serialize)] @@ -78,6 +82,7 @@ pub enum StoredCardUsageType { pub enum PaymentInstrument { Card(CardPayment), CardToken(CardToken), + RawCardForNTI(RawCardDetails), Googlepay(WalletPayment), Applepay(WalletPayment), } @@ -85,15 +90,22 @@ pub enum PaymentInstrument { #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CardPayment { - #[serde(rename = "type")] - pub payment_type: PaymentType, + #[serde(flatten)] + pub raw_card_details: RawCardDetails, + pub cvc: Secret, #[serde(skip_serializing_if = "Option::is_none")] pub card_holder_name: Option>, - pub card_number: cards::CardNumber, - pub expiry_date: ExpiryDate, #[serde(skip_serializing_if = "Option::is_none")] pub billing_address: Option, - pub cvc: Secret, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RawCardDetails { + #[serde(rename = "type")] + pub payment_type: PaymentType, + pub card_number: cards::CardNumber, + pub expiry_date: ExpiryDate, } #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs index 5e9fb0304243..b4d6a3704016 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/response.rs @@ -43,6 +43,8 @@ pub struct AuthorizedResponse { pub fraud: Option, /// Mandate's token pub token: Option, + /// Network transaction ID + pub scheme_reference: Option>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -429,9 +431,13 @@ pub struct WorldpayWebhookTransactionId { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EventDetails { - pub transaction_reference: String, #[serde(rename = "type")] pub event_type: EventType, + pub transaction_reference: String, + /// Mandate's token + pub token: Option, + /// Network transaction ID + pub scheme_reference: Option>, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index ec23b520a200..cd028e81ef1b 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -76,12 +76,14 @@ fn fetch_payment_instrument( ) -> CustomResult { match payment_method { PaymentMethodData::Card(card) => Ok(PaymentInstrument::Card(CardPayment { - payment_type: PaymentType::Plain, - expiry_date: ExpiryDate { - month: card.get_expiry_month_as_i8()?, - year: card.get_expiry_year_as_4_digit_i32()?, + raw_card_details: RawCardDetails { + payment_type: PaymentType::Plain, + expiry_date: ExpiryDate { + month: card.get_expiry_month_as_i8()?, + year: card.get_expiry_year_as_4_digit_i32()?, + }, + card_number: card.card_number, }, - card_number: card.card_number, cvc: card.card_cvc, card_holder_name: billing_address.and_then(|address| address.get_optional_full_name()), billing_address: if let Some(address) = @@ -107,6 +109,16 @@ fn fetch_payment_instrument( None }, })), + PaymentMethodData::CardDetailsForNetworkTransactionId(raw_card_details) => { + Ok(PaymentInstrument::RawCardForNTI(RawCardDetails { + payment_type: PaymentType::Plain, + expiry_date: ExpiryDate { + month: raw_card_details.get_expiry_month_as_i8()?, + year: raw_card_details.get_expiry_year_as_4_digit_i32()?, + }, + card_number: raw_card_details.card_number, + })) + } PaymentMethodData::MandatePayment => mandate_ids .and_then(|mandate_ids| { mandate_ids @@ -185,13 +197,10 @@ fn fetch_payment_instrument( | PaymentMethodData::GiftCard(_) | PaymentMethodData::OpenBanking(_) | PaymentMethodData::CardToken(_) - | PaymentMethodData::NetworkToken(_) - | PaymentMethodData::CardDetailsForNetworkTransactionId(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("worldpay"), - ) - .into()) - } + | PaymentMethodData::NetworkToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("worldpay"), + ) + .into()), } } @@ -439,6 +448,7 @@ fn get_token_and_agreement( payment_method_data: &PaymentMethodData, setup_future_usage: Option, off_session: Option, + mandate_ids: Option, ) -> (Option, Option) { match (payment_method_data, setup_future_usage, off_session) { // CIT @@ -448,7 +458,8 @@ fn get_token_and_agreement( }), Some(CustomerAgreement { agreement_type: CustomerAgreementType::Subscription, - stored_card_usage: StoredCardUsageType::First, + stored_card_usage: Some(StoredCardUsageType::First), + scheme_reference: None, }), ), // MIT @@ -456,7 +467,26 @@ fn get_token_and_agreement( None, Some(CustomerAgreement { agreement_type: CustomerAgreementType::Subscription, - stored_card_usage: StoredCardUsageType::Subsequent, + stored_card_usage: Some(StoredCardUsageType::Subsequent), + scheme_reference: None, + }), + ), + // NTI with raw card data + (PaymentMethodData::CardDetailsForNetworkTransactionId(_), _, _) => ( + None, + mandate_ids.and_then(|mandate_ids| { + mandate_ids + .mandate_reference_id + .and_then(|mandate_id| match mandate_id { + MandateReferenceId::NetworkMandateId(network_transaction_id) => { + Some(CustomerAgreement { + agreement_type: CustomerAgreementType::Unscheduled, + scheme_reference: Some(network_transaction_id.into()), + stored_card_usage: None, + }) + } + _ => None, + }) }), ), _ => (None, None), @@ -487,6 +517,7 @@ impl TryFrom<(&WorldpayRouterData<&T>, &Secret ), ) -> Result { let (router_data, optional_correlation_id) = item; - let (description, redirection_data, mandate_reference, error) = router_data + let (description, redirection_data, mandate_reference, network_txn_id, error) = router_data .response .other_fields .as_ref() @@ -660,6 +691,7 @@ impl mandate_metadata: None, connector_mandate_request_reference_id: None, }), + res.scheme_reference.clone(), None, ), WorldpayPaymentResponseFields::DDCResponse(res) => ( @@ -681,6 +713,7 @@ impl }), None, None, + None, ), WorldpayPaymentResponseFields::ThreeDsChallenged(res) => ( None, @@ -694,16 +727,18 @@ impl }), None, None, + None, ), WorldpayPaymentResponseFields::RefusedResponse(res) => ( None, None, None, + None, Some((res.refusal_code.clone(), res.refusal_description.clone())), ), - WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None, None), + WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None, None, None), }) - .unwrap_or((None, None, None, None)); + .unwrap_or((None, None, None, None, None)); let worldpay_status = router_data.response.outcome.clone(); let optional_error_message = match worldpay_status { PaymentOutcome::ThreeDsAuthenticationFailed => { @@ -725,7 +760,7 @@ impl redirection_data: Box::new(redirection_data), mandate_reference: Box::new(mandate_reference), connector_metadata: None, - network_txn_id: None, + network_txn_id: network_txn_id.map(|id| id.expose()), connector_response_reference_id: optional_correlation_id.clone(), incremental_authorization_allowed: None, charge_id: None, diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 9061a758beab..16d0d748aa91 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -17,7 +17,7 @@ use common_utils::{ use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ address::{Address, AddressDetails, PhoneDetails}, - payment_method_data::{self, Card, PaymentMethodData}, + payment_method_data::{self, Card, CardDetailsForNetworkTransactionId, PaymentMethodData}, router_data::{ ApplePayPredecryptData, ErrorResponse, PaymentMethodToken, RecurringMandatePaymentData, }, @@ -1034,6 +1034,92 @@ impl CardData for Card { } } +impl CardData for CardDetailsForNetworkTransactionId { + fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError> { + let binding = self.card_exp_year.clone(); + let year = binding.peek(); + Ok(Secret::new( + year.get(year.len() - 2..) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(), + )) + } + fn get_card_issuer(&self) -> Result { + get_card_issuer(self.card_number.peek()) + } + fn get_card_expiry_month_year_2_digit_with_delimiter( + &self, + delimiter: String, + ) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?; + Ok(Secret::new(format!( + "{}{}{}", + self.card_exp_month.peek(), + delimiter, + year.peek() + ))) + } + fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret { + let year = self.get_expiry_year_4_digit(); + Secret::new(format!( + "{}{}{}", + year.peek(), + delimiter, + self.card_exp_month.peek() + )) + } + fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret { + let year = self.get_expiry_year_4_digit(); + Secret::new(format!( + "{}{}{}", + self.card_exp_month.peek(), + delimiter, + year.peek() + )) + } + fn get_expiry_year_4_digit(&self) -> Secret { + let mut year = self.card_exp_year.peek().clone(); + if year.len() == 2 { + year = format!("20{}", year); + } + Secret::new(year) + } + fn get_expiry_date_as_yymm(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); + let month = self.card_exp_month.clone().expose(); + Ok(Secret::new(format!("{year}{month}"))) + } + fn get_expiry_date_as_mmyy(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); + let month = self.card_exp_month.clone().expose(); + Ok(Secret::new(format!("{month}{year}"))) + } + fn get_expiry_month_as_i8(&self) -> Result, Error> { + self.card_exp_month + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_expiry_year_as_i32(&self) -> Result, Error> { + self.card_exp_year + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_expiry_year_as_4_digit_i32(&self) -> Result, Error> { + self.get_expiry_year_4_digit() + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } +} + #[track_caller] fn get_card_issuer(card_number: &str) -> Result { for (k, v) in CARD_REGEX.iter() { diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index ec58ab08b878..640a60ae40d4 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -370,7 +370,7 @@ card.credit ={connector_list ="cybersource"} card.debit = {connector_list ="cybersource"} [network_transaction_id_supported_connectors] -connector_list = "stripe,adyen,cybersource" +connector_list = "adyen,cybersource,novalnet,stripe,worldpay" [analytics] source = "sqlx"