diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dce72ec6e8..2d0cf0ad964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ +# 0.3.0 (2023-02-25) + +## Build System / Dependencies + +* **docker-compose:** increase docker health check interval for hyperswitch-server (#534) + +## Chores + +* **release:** port release bug fixes to main branch (#612) (a8d6ce83) + +## Continuous Integration + +* run CI checks on merge queue events (#530) (c7b9e9c1) + +## Documentation Changes + +* **add_connector:** fix typo (#584) (a4f3abf3) + +## New Features + +* **router:** + * include eligible connectors list in list payment methods (#644) (92771b3b) + * API endpoints for managing API keys (#511) (1bdc8955) +* **connector:** + * [Airwallex] add authorize, capture, void, psync, Webhooks support (#646) (6a67dd8b) + * [Bluesnap] add authorize, capture, void, refund, psync, rsync and Webhooks support (#649) (7efdc3c5) + * add authorize, capture, void, refund, psync support for Nuvei (#645) (03a9f5a9) +* Added applepay feature (#636) (1e84c07c) +* add `track_caller` to functions that perform `change_context` (#592) (8d2e573a) +* Redis cache for MCA fetch and update (#515) (963cb528) +* **api_models:** add error structs (#532) (d107b44f) + +## Bug Fixes + +* **connector:** update Bluesnap in routable connectors (#654) (64cb2ffc) +* allow errors with status code 200 to pass (#601) (8a8767e9) +* don't call connector if connector transaction id doesn't exist (#525) (326d6beb) +* throw 500 error when redis goes down (#531) (aafb115a) +* **router:** + * allow setup future usage to be updated in payment update and confirm requests (#610) (#638) (6c128f82) + * feature gate openssl deps for basilisk feature (#536) (e4956820) +* **checkout:** Error Response when wrong api key is passed (#596) (55b6d88a) +* **core:** use guard for access token result (#522) (903b4521) + +## Other Changes + +* **router:** + * webhooks enhancement (#637) (#641) (3bc9feb0) + * api keys path params (#609) (effa7a00) + +## Refactors + +* **router:** + * update payments api contract to accept a list of connectors (#643) (8f1f626c) + * api-key routes refactoring (#600) (e6408276) + * appstate as trait in authentication (#588) (eaf98e66) +* **compatibility:** add additional fields to stripe payment and refund response types (#618) (2ea09e34) +* Throw 500 error on database connection error instead of panic (#527) (f1e3bf48) +* send full payment object for payment sync (#526) (6c2a1fea) +* **middleware:** change visibility to `pub` (#587) (4884a24d) + # 0.2.1 (2023-02-17) ## Fixes diff --git a/Cargo.lock b/Cargo.lock index 5712e1d1cf2..8183ea5d40a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,6 +317,7 @@ dependencies = [ "serde_json", "strum", "time", + "url", "utoipa", ] @@ -1038,6 +1039,7 @@ dependencies = [ "futures", "hex", "masking", + "md5", "nanoid", "once_cell", "proptest", @@ -2222,6 +2224,12 @@ dependencies = [ "syn", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.5.0" diff --git a/config/Development.toml b/config/Development.toml index 68f4630aaef..95e341e72a2 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -43,8 +43,26 @@ locker_decryption_key1 = "" locker_decryption_key2 = "" [connectors.supported] -wallets = ["klarna","braintree","applepay"] -cards = ["stripe","adyen","authorizedotnet","checkout","braintree","aci","shift4","cybersource", "worldpay", "globalpay", "fiserv", "payu", "worldline", "dlocal"] +wallets = ["klarna", "braintree", "applepay"] +cards = [ + "aci", + "adyen", + "airwallex", + "authorizedotnet", + "bluesnap", + "braintree", + "checkout", + "cybersource", + "dlocal", + "fiserv", + "globalpay", + "nuvei", + "payu", + "shift4", + "stripe", + "worldline", + "worldpay", +] [refund] max_attempts = 10 @@ -104,6 +122,15 @@ base_url = "https://apis.sandbox.globalpay.com/ucp/" [connectors.worldline] base_url = "https://eu.sandbox.api-ingenico.com/" +[connectors.bluesnap] +base_url = "https://sandbox.bluesnap.com/" + +[connectors.nuvei] +base_url = "https://ppp-test.nuvei.com/" + +[connectors.airwallex] +base_url = "https://api-demo.airwallex.com/" + [connectors.dlocal] base_url = "https://sandbox.dlocal.com/" diff --git a/config/config.example.toml b/config/config.example.toml index 9a9c22dc65d..b6bfd1dd1cf 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -153,10 +153,19 @@ base_url = "https://try.access.worldpay.com/" [connectors.globalpay] base_url = "https://apis.sandbox.globalpay.com/ucp/" -# This data is used to call respective connectors for wallets and cards +[connectors.bluesnap] +base_url = "https://sandbox.bluesnap.com/" + +[connectors.nuvei] +base_url = "https://ppp-test.nuvei.com/" + +[connectors.airwallex] +base_url = "https://api-demo.airwallex.com/" + [connectors.dlocal] base_url = "https://sandbox.dlocal.com/" +# This data is used to call respective connectors for wallets and cards [connectors.supported] wallets = ["klarna", "braintree", "applepay"] cards = [ diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 9bbac7af768..06f3d3cfb76 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -105,12 +105,38 @@ base_url = "https://try.access.worldpay.com/" [connectors.globalpay] base_url = "https://apis.sandbox.globalpay.com/ucp/" +[connectors.bluesnap] +base_url = "https://sandbox.bluesnap.com/" + +[connectors.nuvei] +base_url = "https://ppp-test.nuvei.com/" + +[connectors.airwallex] +base_url = "https://api-demo.airwallex.com/" + [connectors.dlocal] base_url = "https://sandbox.dlocal.com/" [connectors.supported] wallets = ["klarna", "braintree", "applepay"] -cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "shift4", "cybersource", "worldpay", "globalpay", "fiserv", "dlocal"] +cards = [ + "adyen", + "airwallex", + "authorizedotnet", + "bluesnap", + "braintree", + "checkout", + "cybersource", + "dlocal", + "fiserv", + "globalpay", + "nuvei", + "payu", + "shift4", + "stripe", + "worldline", + "worldpay", +] [scheduler] @@ -118,4 +144,4 @@ stream = "SCHEDULER_STREAM" [scheduler.consumer] disabled = false -consumer_group = "SCHEDULER_GROUP" \ No newline at end of file +consumer_group = "SCHEDULER_GROUP" diff --git a/connector-template/mod.rs b/connector-template/mod.rs index ee78464f025..bbb2d4a6721 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -110,10 +110,14 @@ impl fn get_url(&self, _req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - + fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData) -> CustomResult,errors::ConnectorError> { + let req_obj = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest::try_from(req)?; let {{project-name | downcase}}_req = - utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest>::convert_and_encode(req).change_context(errors::ConnectorError::RequestEncodingFailed)?; + utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest>::encode_to_string_of_json( + &req_obj, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some({{project-name | downcase}}_req)) } diff --git a/connector-template/test.rs b/connector-template/test.rs index c9c5fe0b5f0..df5dde5867a 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -106,6 +106,7 @@ async fn should_void_authorized_payment() { Some(types::PaymentsCancelData { connector_transaction_id: String::from(""), cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() }), None, ) diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 6575cf2a23d..c50967e7a92 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -16,6 +16,7 @@ serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.17", features = ["serde", "serde-well-known", "std"] } +url = { version = "2.3.1", features = ["serde"] } utoipa = { version = "3.0.1", features = ["preserve_order"] } # First party crates diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index e424af80863..6f02a8713ba 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1,6 +1,7 @@ use common_utils::pii; use masking::{Secret, StrongSecret}; use serde::{Deserialize, Serialize}; +use url; use utoipa::ToSchema; use super::payments::AddressDetails; @@ -26,7 +27,7 @@ pub struct CreateMerchantAccount { /// The URL to redirect after the completion of the operation #[schema(max_length = 255, example = "https://www.example.com/success")] - pub return_url: Option, + pub return_url: Option, /// Webhook related details pub webhook_details: Option, diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 08368491925..033a1ce39d8 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -11,6 +11,7 @@ use utoipa::ToSchema; serde::Serialize, strum::Display, strum::EnumString, + frunk::LabelledGeneric, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] @@ -572,8 +573,10 @@ pub enum MandateStatus { pub enum Connector { Aci, Adyen, + Airwallex, Applepay, Authorizedotnet, + Bluesnap, Braintree, Checkout, Cybersource, @@ -583,6 +586,7 @@ pub enum Connector { Fiserv, Globalpay, Klarna, + Nuvei, Payu, Rapyd, Shift4, @@ -593,7 +597,7 @@ pub enum Connector { impl Connector { pub fn supports_access_token(&self) -> bool { - matches!(self, Self::Globalpay | Self::Payu) + matches!(self, Self::Airwallex | Self::Globalpay | Self::Payu) } } @@ -614,7 +618,9 @@ impl Connector { pub enum RoutableConnectors { Aci, Adyen, + Airwallex, Authorizedotnet, + Bluesnap, Braintree, Checkout, Cybersource, @@ -622,6 +628,7 @@ pub enum RoutableConnectors { Fiserv, Globalpay, Klarna, + Nuvei, Payu, Rapyd, Shift4, diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 2967f736e4e..4bbbf33a6d9 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -296,7 +296,7 @@ pub struct ListPaymentMethodResponse { } ] ))] - pub payment_methods: HashSet, + pub payment_methods: Vec, } #[derive(Eq, PartialEq, Hash, Debug, serde::Deserialize, ToSchema)] @@ -362,9 +362,12 @@ pub struct ListPaymentMethod { /// Type of payment experience enabled with the connector #[schema(value_type = Option>, example = json!(["redirect_to_url"]))] pub payment_experience: Option>, + + /// Eligible connectors for this payment method + #[schema(example = json!(["stripe", "adyen"]))] + pub eligible_connectors: Option>, } -/// We need a custom serializer to only send relevant fields in ListPaymentMethodResponse /// Currently if the payment method is Wallet or Paylater the relevant fields are `payment_method` /// and `payment_method_issuers`. Otherwise only consider /// `payment_method`,`payment_method_issuers`,`payment_method_types`,`payment_schemes` fields. @@ -377,6 +380,7 @@ impl serde::Serialize for ListPaymentMethod { let mut state = serializer.serialize_struct("ListPaymentMethod", 4)?; state.serialize_field("payment_method", &self.payment_method)?; state.serialize_field("payment_experience", &self.payment_experience)?; + state.serialize_field("eligible_connectors", &self.eligible_connectors)?; match self.payment_method { api_enums::PaymentMethodType::Wallet | api_enums::PaymentMethodType::PayLater => { state.serialize_field("payment_method_issuers", &self.payment_method_issuers)?; diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 142bef10b42..3adcc5e315d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1,6 +1,6 @@ use std::num::NonZeroI64; -use common_utils::{errors, ext_traits::Encode, pii}; +use common_utils::pii; use masking::{PeekInterface, Secret}; use router_derive::Setter; use time::PrimitiveDateTime; @@ -38,7 +38,7 @@ pub struct PaymentsRequest { pub amount: Option, /// This allows the merchant to manually select a connector with which the payment can go through #[schema(value_type = Option, max_length = 255, example = "stripe")] - pub connector: Option, + pub connector: Option>, /// The currency of the payment request can be specified here #[schema(value_type = Option, example = "USD")] pub currency: Option, @@ -80,7 +80,7 @@ pub struct PaymentsRequest { pub description: Option, /// The URL to redirect after the completion of the operation #[schema(example = "https://hyperswitch.io")] - pub return_url: Option, + pub return_url: Option, /// Indicates that you intend to make future payments with this Payment’s payment method. Providing this parameter will attach the payment method to the Customer, if present, after the Payment is confirmed and any required actions from the user are complete. #[schema(value_type = Option, example = "off_session")] pub setup_future_usage: Option, @@ -795,46 +795,6 @@ impl From<&VerifyRequest> for MandateValidationFields { } } -impl TryFrom for PaymentsResponse { - type Error = error_stack::Report; - fn try_from(item: PaymentsRequest) -> Result { - let payment_id = match item.payment_id { - Some(PaymentIdType::PaymentIntentId(id)) => Some(id), - _ => None, - }; - let metadata = item - .metadata - .as_ref() - .map(Encode::::encode_to_value) - .transpose()?; - Ok(Self { - payment_id, - merchant_id: item.merchant_id, - setup_future_usage: item.setup_future_usage, - off_session: item.off_session, - shipping: item.shipping, - billing: item.billing, - metadata, - capture_method: item.capture_method, - payment_method: item.payment_method, - capture_on: item.capture_on, - payment_method_data: item - .payment_method_data - .map(PaymentMethodDataResponse::from), - email: item.email, - name: item.name, - phone: item.phone, - payment_token: item.payment_token, - return_url: item.return_url, - authentication_type: item.authentication_type, - statement_descriptor_name: item.statement_descriptor_name, - statement_descriptor_suffix: item.statement_descriptor_suffix, - mandate_data: item.mandate_data, - ..Default::default() - }) - } -} - impl From for VerifyResponse { fn from(item: VerifyRequest) -> Self { Self { @@ -853,25 +813,6 @@ impl From for VerifyResponse { } } -impl From for PaymentsResponse { - fn from(item: PaymentsStartRequest) -> Self { - Self { - payment_id: Some(item.payment_id), - merchant_id: Some(item.merchant_id), - ..Default::default() - } - } -} - -impl From for PaymentsResponse { - fn from(item: PaymentsSessionRequest) -> Self { - Self { - payment_id: Some(item.payment_id), - ..Default::default() - } - } -} - impl From for PaymentsSessionResponse { fn from(item: PaymentsSessionRequest) -> Self { let client_secret: Secret = Secret::new(item.client_secret); @@ -893,43 +834,6 @@ impl From for PaymentsRequest { } } -impl From for PaymentsResponse { - // After removing the request from the payments_to_payments_response this will no longer be needed - fn from(item: PaymentsRetrieveRequest) -> Self { - let payment_id = match item.resource_id { - PaymentIdType::PaymentIntentId(id) => Some(id), - _ => None, - }; - - Self { - payment_id, - merchant_id: item.merchant_id, - ..Default::default() - } - } -} - -impl From for PaymentsResponse { - fn from(item: PaymentsCancelRequest) -> Self { - Self { - payment_id: Some(item.payment_id), - cancellation_reason: item.cancellation_reason, - ..Default::default() - } - } -} - -impl From for PaymentsResponse { - // After removing the request from the payments_to_payments_response this will no longer be needed - fn from(item: PaymentsCaptureRequest) -> Self { - Self { - payment_id: item.payment_id, - amount_received: item.amount_to_capture, - ..Self::default() - } - } -} - impl From for CardResponse { fn from(card: Card) -> Self { let card_number_length = card.card_number.peek().clone().len(); @@ -1096,18 +1000,18 @@ pub struct GpaySessionTokenData { #[serde(rename_all = "lowercase")] pub enum SessionToken { /// The session response structure for Google Pay - Gpay(Box), + Gpay(Box), /// The session response structure for Klarna - Klarna(Box), + Klarna(Box), /// The session response structure for PayPal - Paypal(Box), + Paypal(Box), /// The session response structure for Apple Pay - Applepay(Box), + Applepay(Box), } #[derive(Debug, Clone, serde::Serialize, ToSchema)] #[serde(rename_all = "lowercase")] -pub struct GpayData { +pub struct GpaySessionTokenResponse { /// The merchant info pub merchant_info: GpayMerchantInfo, /// List of the allowed payment meythods @@ -1118,7 +1022,7 @@ pub struct GpayData { #[derive(Debug, Clone, serde::Serialize, ToSchema)] #[serde(rename_all = "lowercase")] -pub struct KlarnaData { +pub struct KlarnaSessionTokenResponse { /// The session token for Klarna pub session_token: String, /// The identifier for the session @@ -1127,29 +1031,29 @@ pub struct KlarnaData { #[derive(Debug, Clone, serde::Serialize, ToSchema)] #[serde(rename_all = "lowercase")] -pub struct PaypalData { +pub struct PaypalSessionTokenResponse { /// The session token for PayPal pub session_token: String, } #[derive(Debug, Clone, serde::Serialize, ToSchema)] #[serde(rename_all = "lowercase")] -pub struct ApplepayData { +pub struct ApplepaySessionTokenResponse { /// Session object for Apple Pay - pub session_object: ApplePaySessionObject, + pub session_token_data: ApplePaySessionResponse, /// Payment request object for Apple Pay - pub payment_request_object: ApplePayRequest, + pub payment_request_data: ApplePayPaymentRequest, } #[derive(Debug, Clone, serde::Serialize, ToSchema, serde::Deserialize)] -pub struct ApplePaySessionObject { +pub struct ApplePaySessionResponse { /// Timestamp at which session is requested pub epoch_timestamp: u64, /// Timestamp at which session expires pub expires_at: u64, /// The identifier for the merchant session pub merchant_session_identifier: String, - /// Applepay generates unique ID (UUID) value + /// Apple pay generated unique ID (UUID) value pub nonce: String, /// The identifier for the merchant pub merchant_identifier: String, @@ -1168,7 +1072,7 @@ pub struct ApplePaySessionObject { } #[derive(Debug, Clone, serde::Serialize, ToSchema, serde::Deserialize)] -pub struct ApplePayRequest { +pub struct ApplePayPaymentRequest { /// The code for country pub country_code: String, /// The code for currency @@ -1183,11 +1087,11 @@ pub struct ApplePayRequest { #[derive(Debug, Clone, serde::Serialize, ToSchema, serde::Deserialize)] pub struct AmountInfo { - /// the label must be non-empty to pass validation. + /// The label must be the name of the merchant. pub label: String, - /// The type of label + /// A value that indicates whether the line item(Ex: total, tax, discount, or grand total) is final or pending. #[serde(rename = "type")] - pub label_type: String, + pub total_type: String, /// The total amount for the payment pub amount: String, } diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 5ab2eb69870..4d7982f8d0f 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -26,6 +26,7 @@ signal-hook = "0.3.14" tokio = { version = "1.25.0", features = ["macros", "rt-multi-thread"] } thiserror = "1.0.38" time = { version = "0.3.17", features = ["serde", "serde-well-known", "std"] } +md5 = "0.7.0" # First party crates masking = { version = "0.1.0", path = "../masking" } diff --git a/crates/common_utils/src/crypto.rs b/crates/common_utils/src/crypto.rs index d63aed438cf..87eb99ce15e 100644 --- a/crates/common_utils/src/crypto.rs +++ b/crates/common_utils/src/crypto.rs @@ -1,5 +1,6 @@ //! Utilities for cryptographic algorithms use error_stack::{IntoReport, ResultExt}; +use md5; use ring::{aead, hmac}; use crate::errors::{self, CustomResult}; @@ -219,6 +220,10 @@ impl DecodeMessage for GcmAes256 { #[derive(Debug)] pub struct Sha512; +/// Secure Hash Algorithm 256 +#[derive(Debug)] +pub struct Sha256; + /// Trait for generating a digest for SHA pub trait GenerateDigest { /// takes a message and creates a digest for it @@ -231,6 +236,37 @@ impl GenerateDigest for Sha512 { Ok(digest.as_ref().to_vec()) } } +/// MD5 hash function +#[derive(Debug)] +pub struct Md5; + +impl GenerateDigest for Md5 { + fn generate_digest(&self, message: &[u8]) -> CustomResult, errors::CryptoError> { + let digest = md5::compute(message); + Ok(digest.as_ref().to_vec()) + } +} + +impl VerifySignature for Md5 { + fn verify_signature( + &self, + _secret: &[u8], + signature: &[u8], + msg: &[u8], + ) -> CustomResult { + let hashed_digest = Self + .generate_digest(msg) + .change_context(errors::CryptoError::SignatureVerificationFailed)?; + Ok(hashed_digest == signature) + } +} + +impl GenerateDigest for Sha256 { + fn generate_digest(&self, message: &[u8]) -> CustomResult, errors::CryptoError> { + let digest = ring::digest::digest(&ring::digest::SHA256, message); + Ok(digest.as_ref().to_vec()) + } +} /// Generate a random string using a cryptographically secure pseudo-random number generator /// (CSPRNG). Typically used for generating (readable) keys and passwords. @@ -256,6 +292,7 @@ pub fn generate_cryptographically_secure_random_bytes() -> [u8; mod crypto_tests { #![allow(clippy::expect_used)] use super::{DecodeMessage, EncodeMessage, SignMessage, VerifySignature}; + use crate::crypto::GenerateDigest; #[test] fn test_hmac_sha256_sign_message() { @@ -387,4 +424,39 @@ mod crypto_tests { assert!(err_decoded.is_err()); } + + #[test] + fn test_md5_digest() { + let message = "abcdefghijklmnopqrstuvwxyz".as_bytes(); + assert_eq!( + format!( + "{}", + hex::encode(super::Md5.generate_digest(message).expect("Digest")) + ), + "c3fcd3d76192e4007dfb496cca67e13b" + ); + } + + #[test] + fn test_md5_verify_signature() { + let right_signature = + hex::decode("c3fcd3d76192e4007dfb496cca67e13b").expect("signature decoding"); + let wrong_signature = + hex::decode("d5550730377011948f12cc28889bee590d2a5434d6f54b87562f2dbc2657823f") + .expect("Wrong signature decoding"); + let secret = "".as_bytes(); + let data = "abcdefghijklmnopqrstuvwxyz".as_bytes(); + + let right_verified = super::Md5 + .verify_signature(secret, &right_signature, data) + .expect("Right signature verification result"); + + assert!(right_verified); + + let wrong_verified = super::Md5 + .verify_signature(secret, &wrong_signature, data) + .expect("Wrong signature verification result"); + + assert!(!wrong_verified); + } } diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index b64ad922ef9..6ec1856e960 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -49,6 +49,28 @@ pub mod date_time { (result, start.elapsed().as_seconds_f64() * 1000f64) } + /// Prefix the date field with zero if it has single digit Eg: 1 -> 01 + fn prefix_zero(input: u8) -> String { + if input < 10 { + return "0".to_owned() + input.to_string().as_str(); + } + input.to_string() + } + + /// Return the current date and time in UTC with the format YYYYMMDDHHmmss Eg: 20191105081132 + pub fn date_as_yyyymmddhhmmss() -> String { + let now = OffsetDateTime::now_utc(); + format!( + "{}{}{}{}{}{}", + now.year(), + prefix_zero(u8::from(now.month())), + prefix_zero(now.day()), + prefix_zero(now.hour()), + prefix_zero(now.minute()), + prefix_zero(now.second()) + ) + } + /// Return the current date and time in UTC with the format [year]-[month]-[day]T[hour]:[minute]:[second].mmmZ Eg: 2023-02-15T13:33:18.898Z pub fn date_as_yyyymmddthhmmssmmmz() -> Result { const ISO_CONFIG: EncodedConfig = Config::DEFAULT diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index f031f2e738f..cc6c5ba5e01 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -166,8 +166,20 @@ pub enum StripeErrorCode { }, #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "The mandate information is invalid. {message}")] PaymentIntentMandateInvalid { message: String }, + #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "The payment with the specified payment_id '{payment_id}' already exists in our records.")] DuplicatePayment { payment_id: String }, + + #[error(error_type = StripeErrorType::ConnectorError, code = "", message = "{code}: {message}")] + ExternalConnectorError { + code: String, + message: String, + connector: String, + status_code: u16, + }, + + #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "The connector provided in the request is incorrect or not available")] + IncorrectConnectorNameGiven, // [#216]: https://github.com/juspay/hyperswitch/issues/216 // Implement the remaining stripe error codes @@ -323,6 +335,8 @@ pub enum StripeErrorType { ApiError, CardError, InvalidRequestError, + ConnectorError, + HyperswitchError, } impl From for StripeErrorCode { @@ -369,8 +383,21 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::RefundFailed { data } => Self::RefundFailed, // Nothing at stripe to map errors::ApiErrorResponse::InternalServerError => Self::InternalServerError, // not a stripe code - errors::ApiErrorResponse::ExternalConnectorError { .. } => Self::InternalServerError, - errors::ApiErrorResponse::IncorrectConnectorNameGiven => Self::InternalServerError, + errors::ApiErrorResponse::ExternalConnectorError { + code, + message, + connector, + status_code, + .. + } => Self::ExternalConnectorError { + code, + message, + connector, + status_code, + }, + errors::ApiErrorResponse::IncorrectConnectorNameGiven => { + Self::IncorrectConnectorNameGiven + } errors::ApiErrorResponse::MandateActive => Self::MandateActive, //not a stripe code errors::ApiErrorResponse::CustomerRedacted => Self::CustomerRedacted, //not a stripe code errors::ApiErrorResponse::ConfigNotFound => Self::ConfigNotFound, // not a stripe code @@ -474,12 +501,16 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::ResourceIdNotFound | Self::PaymentIntentMandateInvalid { .. } | Self::PaymentIntentUnexpectedState { .. } - | Self::DuplicatePayment { .. } => StatusCode::BAD_REQUEST, + | Self::DuplicatePayment { .. } + | Self::IncorrectConnectorNameGiven => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::InternalServerError | Self::MandateActive | Self::CustomerRedacted => StatusCode::INTERNAL_SERVER_ERROR, Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, + Self::ExternalConnectorError { status_code, .. } => { + StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) + } } } diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index ee98ccc81be..f2e27ab3b95 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -116,7 +116,7 @@ impl From for payments::Address { #[derive(PartialEq, Eq, Deserialize, Clone)] pub struct StripePaymentIntentRequest { pub amount: Option, //amount in cents, hence passed as integer - pub connector: Option, + pub connector: Option>, pub currency: Option, #[serde(rename = "amount_to_capture")] pub amount_capturable: Option, @@ -126,7 +126,7 @@ pub struct StripePaymentIntentRequest { pub description: Option, pub payment_method_data: Option, pub receipt_email: Option>, - pub return_url: Option, + pub return_url: Option, pub setup_future_usage: Option, pub shipping: Option, pub billing_details: Option, @@ -265,40 +265,110 @@ pub struct StripeCaptureRequest { #[derive(Default, Eq, PartialEq, Serialize)] pub struct StripePaymentIntentResponse { pub id: Option, - pub object: String, + pub object: &'static str, pub amount: i64, pub amount_received: Option, pub amount_capturable: Option, pub currency: String, pub status: StripePaymentStatus, pub client_secret: Option>, - #[serde(with = "common_utils::custom_serde::iso8601::option")] - pub created: Option, + pub created: Option, pub customer: Option, pub refunds: Option>, pub mandate_id: Option, pub metadata: Option, + pub charges: Charges, + pub connector: Option, + pub description: Option, + pub mandate_data: Option, + pub setup_future_usage: Option, + pub off_session: Option, + pub return_url: Option, + pub authentication_type: Option, + pub next_action: Option, + pub cancellation_reason: Option, + pub payment_method: Option, + pub payment_method_data: Option, + pub shipping: Option, + pub billing: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub capture_on: Option, + pub payment_token: Option, + pub email: Option>, + pub phone: Option>, + pub error_code: Option, + pub error_message: Option, + pub statement_descriptor_suffix: Option, + pub statement_descriptor_name: Option, + pub capture_method: Option, + pub name: Option>, } impl From for StripePaymentIntentResponse { fn from(resp: payments::PaymentsResponse) -> Self { Self { - object: "payment_intent".to_owned(), + object: "payment_intent", + id: resp.payment_id, + status: StripePaymentStatus::from(resp.status), amount: resp.amount, - amount_received: resp.amount_received, amount_capturable: resp.amount_capturable, - currency: resp.currency.to_lowercase(), - status: StripePaymentStatus::from(resp.status), + amount_received: resp.amount_received, + connector: resp.connector, client_secret: resp.client_secret, - created: resp.created, + created: resp.created.map(|t| t.assume_utc().unix_timestamp()), + currency: resp.currency.to_lowercase(), customer: resp.customer_id, - id: resp.payment_id, + description: resp.description, refunds: resp.refunds, mandate_id: resp.mandate_id, + mandate_data: resp.mandate_data, + setup_future_usage: resp.setup_future_usage, + off_session: resp.off_session, + capture_on: resp.capture_on, + capture_method: resp.capture_method, + payment_method: resp.payment_method, + payment_method_data: resp.payment_method_data, + payment_token: resp.payment_token, + shipping: resp.shipping, + billing: resp.billing, + email: resp.email, + name: resp.name, + phone: resp.phone, + return_url: resp.return_url, + authentication_type: resp.authentication_type, + statement_descriptor_name: resp.statement_descriptor_name, + statement_descriptor_suffix: resp.statement_descriptor_suffix, + next_action: resp.next_action, + cancellation_reason: resp.cancellation_reason, + error_code: resp.error_code, + error_message: resp.error_message, metadata: resp.metadata, + charges: Charges::new(), } } } + +#[derive(Default, Eq, PartialEq, Serialize)] +pub struct Charges { + object: &'static str, + data: Vec, + has_more: bool, + total_count: i32, + url: String, +} + +impl Charges { + fn new() -> Self { + Self { + object: "list", + data: vec![], + has_more: false, + total_count: 0, + url: "http://placeholder".to_string(), + } + } +} + #[derive(Clone, Debug, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct StripePaymentListConstraints { diff --git a/crates/router/src/compatibility/stripe/refunds/types.rs b/crates/router/src/compatibility/stripe/refunds/types.rs index fe0cf423257..1b74795a1dd 100644 --- a/crates/router/src/compatibility/stripe/refunds/types.rs +++ b/crates/router/src/compatibility/stripe/refunds/types.rs @@ -2,7 +2,7 @@ use std::{convert::From, default::Default}; use serde::{Deserialize, Serialize}; -use crate::types::api::refunds; +use crate::{core::errors, types::api::refunds}; #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct StripeCreateRefundRequest { @@ -23,6 +23,8 @@ pub struct StripeCreateRefundResponse { pub currency: String, pub payment_intent: String, pub status: StripeRefundStatus, + pub created: Option, + pub metadata: serde_json::Value, } #[derive(Clone, Serialize, Deserialize, Eq, PartialEq)] @@ -40,6 +42,7 @@ impl From for refunds::RefundRequest { amount: req.amount, payment_id: req.payment_intent, reason: req.reason, + refund_type: Some(refunds::RefundType::Instant), ..Default::default() } } @@ -65,14 +68,17 @@ impl From for StripeRefundStatus { } } -impl From for StripeCreateRefundResponse { - fn from(res: refunds::RefundResponse) -> Self { - Self { +impl TryFrom for StripeCreateRefundResponse { + type Error = error_stack::Report; + fn try_from(res: refunds::RefundResponse) -> Result { + Ok(Self { id: res.refund_id, amount: res.amount, currency: res.currency.to_ascii_lowercase(), payment_intent: res.payment_id, status: res.status.into(), - } + created: res.created_at.map(|t| t.assume_utc().unix_timestamp()), + metadata: res.metadata.unwrap_or(serde_json::json!({})), + }) } } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index f296e06826b..7fe3110016c 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -116,7 +116,7 @@ pub struct StripeSetupIntentRequest { pub description: Option, pub payment_method_data: Option, pub receipt_email: Option>, - pub return_url: Option, + pub return_url: Option, pub setup_future_usage: Option, pub shipping: Option, pub billing_details: Option, diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index 5452ebdc6fe..69ca3c5272f 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -23,7 +23,7 @@ where F: Fn(&'b A, U, T) -> Fut, Fut: Future>>, Q: Serialize + std::fmt::Debug + 'a, - S: From + Serialize, + S: TryFrom + Serialize, E: Serialize + error_stack::Context + actix_web::ResponseError + Clone, errors::ApiErrorResponse: ErrorSwitch, T: std::fmt::Debug, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 6ca18641287..e6ba7a8c2fb 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -137,8 +137,10 @@ pub struct SupportedConnectors { pub struct Connectors { pub aci: ConnectorParams, pub adyen: ConnectorParams, + pub airwallex: ConnectorParams, pub applepay: ConnectorParams, pub authorizedotnet: ConnectorParams, + pub bluesnap: ConnectorParams, pub braintree: ConnectorParams, pub checkout: ConnectorParams, pub cybersource: ConnectorParams, @@ -146,6 +148,7 @@ pub struct Connectors { pub fiserv: ConnectorParams, pub globalpay: ConnectorParams, pub klarna: ConnectorParams, + pub nuvei: ConnectorParams, pub payu: ConnectorParams, pub rapyd: ConnectorParams, pub shift4: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 9a4d99049bf..a95930a58e3 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -1,13 +1,16 @@ pub mod aci; pub mod adyen; +pub mod airwallex; pub mod applepay; pub mod authorizedotnet; +pub mod bluesnap; pub mod braintree; pub mod checkout; pub mod cybersource; pub mod fiserv; pub mod globalpay; pub mod klarna; +pub mod nuvei; pub mod payu; pub mod rapyd; pub mod shift4; @@ -19,8 +22,9 @@ pub mod worldpay; pub mod dlocal; pub use self::{ - aci::Aci, adyen::Adyen, applepay::Applepay, authorizedotnet::Authorizedotnet, - braintree::Braintree, checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, - fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna, payu::Payu, rapyd::Rapyd, shift4::Shift4, - stripe::Stripe, worldline::Worldline, worldpay::Worldpay, + aci::Aci, adyen::Adyen, airwallex::Airwallex, applepay::Applepay, + authorizedotnet::Authorizedotnet, bluesnap::Bluesnap, braintree::Braintree, checkout::Checkout, + cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna, + nuvei::Nuvei, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, worldline::Worldline, + worldpay::Worldpay, }; diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs new file mode 100644 index 00000000000..fdd66013481 --- /dev/null +++ b/crates/router/src/connector/airwallex.rs @@ -0,0 +1,916 @@ +mod transformers; + +use std::fmt::Debug; + +use common_utils::ext_traits::ByteSliceExt; +use error_stack::{IntoReport, ResultExt}; +use transformers as airwallex; + +use super::utils::{AccessTokenRequestInfo, RefundsRequestData}; +use crate::{ + configs::settings, + core::{ + errors::{self, CustomResult}, + payments, + }, + db::StorageInterface, + headers, logger, routes, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, RouterData, + }, + utils::{self, crypto, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Airwallex; + +impl ConnectorCommonExt for Airwallex +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut headers = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string(), + )]; + let access_token = req + .access_token + .clone() + .ok_or(errors::ConnectorError::FailedToObtainAuthType)?; + + let auth_header = ( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", access_token.token), + ); + + headers.push(auth_header); + Ok(headers) + } +} + +impl ConnectorCommon for Airwallex { + fn id(&self) -> &'static str { + "airwallex" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.airwallex.base_url.as_ref() + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + logger::debug!(payu_error_response=?res); + let response: airwallex::AirwallexErrorResponse = res + .response + .parse_struct("Airwallex ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.source, + }) + } +} + +impl api::Payment for Airwallex {} + +impl api::PreVerify for Airwallex {} +impl ConnectorIntegration + for Airwallex +{ +} + +impl api::ConnectorAccessToken for Airwallex {} + +impl ConnectorIntegration + for Airwallex +{ + fn get_url( + &self, + _req: &types::RefreshTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "api/v1/authentication/login" + )) + } + + fn get_headers( + &self, + req: &types::RefreshTokenRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let headers = vec![ + (headers::X_API_KEY.to_string(), req.request.app_id.clone()), + ("Content-Length".to_string(), "0".to_string()), + ("x-client-id".to_string(), req.get_request_id()?), + ]; + Ok(headers) + } + + fn build_request( + &self, + req: &types::RefreshTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req = Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .headers(types::RefreshTokenType::get_headers(self, req, connectors)?) + .url(&types::RefreshTokenType::get_url(self, req, connectors)?) + .body(types::RefreshTokenType::get_request_body(self, req)?) + .build(), + ); + logger::debug!(payu_access_token_request=?req); + Ok(req) + } + fn handle_response( + &self, + data: &types::RefreshTokenRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(access_token_response=?res); + let response: airwallex::AirwallexAuthUpdateResponse = res + .response + .parse_struct("airwallex AirwallexAuthUpdateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + logger::debug!(access_token_error_response=?res); + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + > for Airwallex +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeSessionTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeSessionTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "api/v1/pa/payment_intents/create" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeSessionTokenRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = airwallex::AirwallexIntentRequest::try_from(req)?; + let req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeSessionTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPreAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsPreAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsPreAuthorizeType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RouterData< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + res: Response, + ) -> CustomResult< + RouterData< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > + where + api::AuthorizeSessionToken: Clone, + types::AuthorizeSessionTokenData: Clone, + types::PaymentsResponseData: Clone, + { + let response: airwallex::AirwallexPaymentsResponse = res + .response + .parse_struct("airwallex AirwallexPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(nuvei_session_response=?response); + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentAuthorize for Airwallex {} + +#[async_trait::async_trait] +impl ConnectorIntegration + for Airwallex +{ + async fn execute_pretasks( + &self, + router_data: &mut types::PaymentsAuthorizeRouterData, + app_state: &routes::AppState, + ) -> CustomResult<(), errors::ConnectorError> { + let integ: Box< + &(dyn ConnectorIntegration< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + > + Send + + Sync + + 'static), + > = Box::new(&Self); + let authorize_data = &types::PaymentsAuthorizeSessionTokenRouterData::from(&router_data); + let resp = services::execute_connector_processing_step( + app_state, + integ, + authorize_data, + payments::CallConnectorAction::Trigger, + ) + .await?; + router_data.reference_id = resp.reference_id; + Ok(()) + } + + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}{}", + self.base_url(connectors), + "api/v1/pa/payment_intents/", + req.reference_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?, + "/confirm" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let airwallex_req = + utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(airwallex_req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: airwallex::AirwallexPaymentsResponse = res + .response + .parse_struct("AirwallexPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(airwallexpayments_create_response=?response); + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentSync for Airwallex {} +impl ConnectorIntegration + for Airwallex +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "api/v1/pa/payment_intents/", + connector_payment_id, + )) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(payment_sync_response=?res); + let response: airwallex::AirwallexPaymentsResponse = res + .response + .parse_struct("airwallex PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentCapture for Airwallex {} +impl ConnectorIntegration + for Airwallex +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}{}", + self.base_url(connectors), + "api/v1/pa/payment_intents/", + req.request.connector_transaction_id, + "/capture" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = airwallex::AirwallexPaymentsCaptureRequest::try_from(req)?; + let airwallex_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(airwallex_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: airwallex::AirwallexPaymentsResponse = res + .response + .parse_struct("Airwallex PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(airwallexpayments_create_response=?response); + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentSession for Airwallex {} + +impl ConnectorIntegration + for Airwallex +{ + //TODO: implement sessions flow +} + +impl api::PaymentVoid for Airwallex {} + +impl ConnectorIntegration + for Airwallex +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}{}", + self.base_url(connectors), + "api/v1/pa/payment_intents/", + req.request.connector_transaction_id, + "/cancel" + )) + } + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = airwallex::AirwallexPaymentsCancelRequest::try_from(req)?; + let airwallex_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(airwallex_req)) + } + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: airwallex::AirwallexPaymentsResponse = res + .response + .parse_struct("Airwallex PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(airwallexpayments_create_response=?response); + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body(self, req)?) + .build(), + )) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::Refund for Airwallex {} +impl api::RefundExecute for Airwallex {} +impl api::RefundSync for Airwallex {} + +impl ConnectorIntegration + for Airwallex +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "api/v1/pa/refunds/create" + )) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let airwallex_req = + utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(airwallex_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + logger::debug!(target: "router::connector::airwallex", response=?res); + let response: airwallex::RefundResponse = res + .response + .parse_struct("airwallex RefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Airwallex +{ + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "/api/v1/pa/refunds/", + req.request.get_connector_refund_id()? + )) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(target: "router::connector::airwallex", response=?res); + let response: airwallex::RefundResponse = res + .response + .parse_struct("airwallex RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Airwallex { + fn get_webhook_source_verification_algorithm( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let security_header = request + .headers + .get("x-signature") + .map(|header_value| { + header_value + .to_str() + .map(String::from) + .map_err(|_| errors::ConnectorError::WebhookSignatureNotFound) + .into_report() + }) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound) + .into_report()??; + + hex::decode(security_header) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _secret: &[u8], + ) -> CustomResult, errors::ConnectorError> { + let timestamp = request + .headers + .get("x-timestamp") + .map(|header_value| { + header_value + .to_str() + .map(String::from) + .map_err(|_| errors::ConnectorError::WebhookSignatureNotFound) + .into_report() + }) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound) + .into_report()??; + + Ok(format!("{}{}", timestamp, String::from_utf8_lossy(request.body)).into_bytes()) + } + + async fn get_webhook_source_verification_merchant_secret( + &self, + db: &dyn StorageInterface, + merchant_id: &str, + ) -> CustomResult, errors::ConnectorError> { + let key = format!("whsec_verification_{}_{}", self.id(), merchant_id); + let secret = db + .find_config_by_key(&key) + .await + .change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?; + Ok(secret.config.into_bytes()) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let details: airwallex::AirwallexWebhookData = request + .body + .parse_struct("airwallexWebhookData") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + Ok(details.source_id) + } + + fn get_webhook_event_type( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let details: airwallex::AirwallexWebhookData = request + .body + .parse_struct("airwallexWebhookData") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + Ok(match details.name.as_str() { + "payment_attempt.failed_to_process" => api::IncomingWebhookEvent::PaymentIntentFailure, + "payment_attempt.authorized" => api::IncomingWebhookEvent::PaymentIntentSuccess, + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound).into_report()?, + }) + } + + fn get_webhook_resource_object( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let details: airwallex::AirwallexWebhookObjectResource = request + .body + .parse_struct("AirwallexWebhookObjectResource") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(details.data.object) + } +} + +impl services::ConnectorRedirectResponse for Airwallex { + fn get_flow_type( + &self, + _query_params: &str, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs new file mode 100644 index 00000000000..97e876bd091 --- /dev/null +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -0,0 +1,386 @@ +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; +use url::Url; +use uuid::Uuid; + +use crate::{ + core::errors, + pii::{self, Secret}, + services, + types::{self, api, storage::enums}, +}; + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct AirwallexIntentRequest { + // Unique ID to be sent for each transaction/operation request to the connector + request_id: String, + amount: i64, + currency: enums::Currency, + //ID created in merchant's order system that corresponds to this PaymentIntent. + merchant_order_id: String, +} +impl TryFrom<&types::PaymentsAuthorizeSessionTokenRouterData> for AirwallexIntentRequest { + type Error = error_stack::Report; + fn try_from( + item: &types::PaymentsAuthorizeSessionTokenRouterData, + ) -> Result { + Ok(Self { + request_id: Uuid::new_v4().to_string(), + amount: item.request.amount, + currency: item.request.currency, + merchant_order_id: item.payment_id.clone(), + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct AirwallexPaymentsRequest { + // Unique ID to be sent for each transaction/operation request to the connector + request_id: String, + payment_method: AirwallexPaymentMethod, + payment_method_options: Option, + return_url: Option, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum AirwallexPaymentMethod { + Card(AirwallexCard), +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct AirwallexCard { + card: AirwallexCardDetails, + #[serde(rename = "type")] + payment_method_type: AirwallexPaymentType, +} +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct AirwallexCardDetails { + expiry_month: Secret, + expiry_year: Secret, + number: Secret, + cvc: Secret, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum AirwallexPaymentType { + Card, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum AirwallexPaymentOptions { + Card(AirwallexCardPaymentOptions), +} +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct AirwallexCardPaymentOptions { + auto_capture: bool, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for AirwallexPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let mut payment_method_options = None; + let payment_method = match item.request.payment_method_data.clone() { + api::PaymentMethod::Card(ccard) => { + payment_method_options = + Some(AirwallexPaymentOptions::Card(AirwallexCardPaymentOptions { + auto_capture: matches!( + item.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + ), + })); + Ok(AirwallexPaymentMethod::Card(AirwallexCard { + card: AirwallexCardDetails { + number: ccard.card_number, + expiry_month: ccard.card_exp_month.clone(), + expiry_year: ccard.card_exp_year.clone(), + cvc: ccard.card_cvc, + }, + payment_method_type: AirwallexPaymentType::Card, + })) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Unknown payment method".to_string(), + )), + }?; + Ok(Self { + request_id: Uuid::new_v4().to_string(), + payment_method, + payment_method_options, + return_url: item.router_return_url.clone(), + }) + } +} + +#[derive(Deserialize)] +pub struct AirwallexAuthUpdateResponse { + #[serde(with = "common_utils::custom_serde::iso8601")] + expires_at: PrimitiveDateTime, + token: String, +} + +impl TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let expires = (item.response.expires_at - common_utils::date_time::now()).whole_seconds(); + Ok(Self { + response: Ok(types::AccessToken { + token: item.response.token, + expires, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct AirwallexPaymentsCaptureRequest { + // Unique ID to be sent for each transaction/operation request to the connector + request_id: String, + amount: Option, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for AirwallexPaymentsCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + Ok(Self { + request_id: Uuid::new_v4().to_string(), + amount: item.request.amount_to_capture, + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct AirwallexPaymentsCancelRequest { + // Unique ID to be sent for each transaction/operation request to the connector + request_id: String, + cancellation_reason: Option, +} + +impl TryFrom<&types::PaymentsCancelRouterData> for AirwallexPaymentsCancelRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + Ok(Self { + request_id: Uuid::new_v4().to_string(), + cancellation_reason: item.request.cancellation_reason.clone(), + }) + } +} + +// PaymentsResponse +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AirwallexPaymentStatus { + Succeeded, + Failed, + #[default] + Pending, + RequiresPaymentMethod, + RequiresCustomerAction, + RequiresCapture, + Cancelled, +} + +impl From for enums::AttemptStatus { + fn from(item: AirwallexPaymentStatus) -> Self { + match item { + AirwallexPaymentStatus::Succeeded => Self::Charged, + AirwallexPaymentStatus::Failed => Self::Failure, + AirwallexPaymentStatus::Pending => Self::Pending, + AirwallexPaymentStatus::RequiresPaymentMethod => Self::PaymentMethodAwaited, + AirwallexPaymentStatus::RequiresCustomerAction => Self::AuthenticationPending, + AirwallexPaymentStatus::RequiresCapture => Self::Authorized, + AirwallexPaymentStatus::Cancelled => Self::Voided, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct AirwallexRedirectFormData { + #[serde(rename = "JWT")] + jwt: String, + #[serde(rename = "threeDSMethodData")] + three_ds_method_data: String, + token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct AirwallexPaymentsNextAction { + url: Url, + method: services::Method, + data: AirwallexRedirectFormData, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct AirwallexPaymentsResponse { + status: AirwallexPaymentStatus, + //Unique identifier for the PaymentIntent + id: String, + amount: Option, + //ID of the PaymentConsent related to this PaymentIntent + payment_consent_id: Option, + next_action: Option, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + AirwallexPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let redirection_data = + item.response + .next_action + .map(|response_url_data| services::RedirectForm { + endpoint: response_url_data.url.to_string(), + method: response_url_data.method, + form_fields: std::collections::HashMap::from([ + ("JWT".to_string(), response_url_data.data.jwt), + ( + "threeDSMethodData".to_string(), + response_url_data.data.three_ds_method_data, + ), + ("token".to_string(), response_url_data.data.token), + ]), + }); + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + reference_id: Some(item.response.id.clone()), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data, + mandate_reference: None, + connector_metadata: None, + }), + ..item.data + }) + } +} + +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct AirwallexRefundRequest { + // Unique ID to be sent for each transaction/operation request to the connector + request_id: String, + amount: Option, + reason: Option, + //Identifier for the PaymentIntent for which Refund is requested + payment_intent_id: String, +} + +impl TryFrom<&types::RefundsRouterData> for AirwallexRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + Ok(Self { + request_id: Uuid::new_v4().to_string(), + amount: Some(item.request.refund_amount), + reason: item.request.reason.clone(), + payment_intent_id: item.request.connector_transaction_id.clone(), + }) + } +} + +// Type definition for Refund Response +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Received, + Accepted, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Received | RefundStatus::Accepted => Self::Pending, + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + //A unique number that tags a credit or debit card transaction when it goes from the merchant's bank through to the cardholder's bank. + acquirer_reference_number: String, + amount: i64, + //Unique identifier for the Refund + id: String, + status: RefundStatus, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund_status = enums::RefundStatus::from(item.response.status); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, + refund_status, + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund_status = enums::RefundStatus::from(item.response.status); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, + refund_status, + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AirwallexWebhookData { + pub source_id: String, + pub name: String, +} + +#[derive(Debug, Deserialize)] +pub struct AirwallexWebhookDataResource { + pub object: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +pub struct AirwallexWebhookObjectResource { + pub data: AirwallexWebhookDataResource, +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct AirwallexErrorResponse { + pub code: String, + pub message: String, + pub details: Option>, + pub source: Option, +} diff --git a/crates/router/src/connector/applepay.rs b/crates/router/src/connector/applepay.rs index 791507fb576..a4570c5d13d 100644 --- a/crates/router/src/connector/applepay.rs +++ b/crates/router/src/connector/applepay.rs @@ -158,7 +158,7 @@ impl data: &types::PaymentsSessionRouterData, res: types::Response, ) -> CustomResult { - let response: applepay::ApplepaySessionResponse = res + let response: applepay::ApplepaySessionTokenResponse = res .response .parse_struct("ApplepaySessionResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -197,11 +197,11 @@ impl .get_required_value("connector_meta_data") .change_context(errors::ConnectorError::NoConnectorMetaData)?; - let metadata: transformers::ApplePayMetaData = metadata + let metadata: transformers::ApplePayMetadata = metadata .parse_value("ApplePayMetaData") .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(metadata.session_object.certificate)) + Ok(Some(metadata.session_token_data.certificate)) } fn get_certificate_key( @@ -214,11 +214,11 @@ impl .get_required_value("connector_meta_data") .change_context(errors::ConnectorError::NoConnectorMetaData)?; - let metadata: transformers::ApplePayMetaData = metadata + let metadata: transformers::ApplePayMetadata = metadata .parse_value("ApplePayMetaData") .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(metadata.session_object.certificate_keys)) + Ok(Some(metadata.session_token_data.certificate_keys)) } } diff --git a/crates/router/src/connector/applepay/transformers.rs b/crates/router/src/connector/applepay/transformers.rs index 3488c2126eb..c3a756daf72 100644 --- a/crates/router/src/connector/applepay/transformers.rs +++ b/crates/router/src/connector/applepay/transformers.rs @@ -16,7 +16,7 @@ pub struct ApplepaySessionRequest { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ApplepaySessionResponse { +pub struct ApplepaySessionTokenResponse { pub epoch_timestamp: u64, pub expires_at: u64, pub merchant_session_identifier: String, @@ -38,20 +38,20 @@ pub struct ErrorResponse { } #[derive(Debug, Default, Serialize, Deserialize)] -pub struct ApplePayMetaData { - pub payment_object: PaymentObjectMetaData, - pub session_object: SessionObject, +pub struct ApplePayMetadata { + pub payment_request_data: PaymentRequestMetadata, + pub session_token_data: SessionRequest, } #[derive(Debug, Default, Serialize, Deserialize)] -pub struct PaymentObjectMetaData { +pub struct PaymentRequestMetadata { pub supported_networks: Vec, pub merchant_capabilities: Vec, pub label: String, } #[derive(Debug, Default, Serialize, Deserialize)] -pub struct SessionObject { +pub struct SessionRequest { pub certificate: String, pub certificate_keys: String, pub merchant_identifier: String, @@ -88,15 +88,15 @@ impl TryFrom<&types::PaymentsSessionRouterData> for ApplepaySessionRequest { .get_required_value("connector_meta_data") .change_context(errors::ConnectorError::NoConnectorMetaData)?; - let metadata: ApplePayMetaData = metadata - .parse_value("ApplePayMetaData") + let metadata: ApplePayMetadata = metadata + .parse_value("ApplePayMetadata") .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Self { - merchant_identifier: metadata.session_object.merchant_identifier, - display_name: metadata.session_object.display_name, - initiative: metadata.session_object.initiative, - initiative_context: metadata.session_object.initiative_context, + merchant_identifier: metadata.session_token_data.merchant_identifier, + display_name: metadata.session_token_data.display_name, + initiative: metadata.session_token_data.initiative, + initiative_context: metadata.session_token_data.initiative_context, }) } } @@ -105,7 +105,7 @@ impl TryFrom< types::ResponseRouterData< F, - ApplepaySessionResponse, + ApplepaySessionTokenResponse, types::PaymentsSessionData, types::PaymentsResponseData, >, @@ -115,7 +115,7 @@ impl fn try_from( item: types::ResponseRouterData< F, - ApplepaySessionResponse, + ApplepaySessionTokenResponse, types::PaymentsSessionData, types::PaymentsResponseData, >, @@ -127,12 +127,12 @@ impl .get_required_value("connector_meta_data") .change_context(errors::ConnectorError::NoConnectorMetaData)?; - let metadata: ApplePayMetaData = metadata - .parse_value("ApplePayMetaData") + let metadata: ApplePayMetadata = metadata + .parse_value("ApplePayMetadata") .change_context(errors::ConnectorError::RequestEncodingFailed)?; let amount_info = AmountInfo { - label: metadata.payment_object.label, + label: metadata.payment_request_data.label, label_type: "final".to_string(), amount: (item.data.request.amount / 100).to_string(), }; @@ -149,12 +149,12 @@ impl })?, currency_code: item.data.request.currency.to_string(), total: amount_info, - merchant_capabilities: metadata.payment_object.merchant_capabilities, - supported_networks: metadata.payment_object.supported_networks, - apple_pay_merchant_id: metadata.session_object.merchant_identifier, + merchant_capabilities: metadata.payment_request_data.merchant_capabilities, + supported_networks: metadata.payment_request_data.supported_networks, + apple_pay_merchant_id: metadata.session_token_data.merchant_identifier, }; - let applepay_session_object = ApplepaySessionResponse { + let applepay_session = ApplepaySessionTokenResponse { epoch_timestamp: item.response.epoch_timestamp, expires_at: item.response.expires_at, merchant_session_identifier: item.response.merchant_session_identifier, @@ -171,10 +171,12 @@ impl Ok(Self { response: Ok(types::PaymentsResponseData::SessionResponse { session_token: { - api_models::payments::SessionToken::Applepay(Box::new(payments::ApplepayData { - session_object: applepay_session_object.into(), - payment_request_object: payment_request.into(), - })) + api_models::payments::SessionToken::Applepay(Box::new( + payments::ApplepaySessionTokenResponse { + session_token_data: applepay_session.into(), + payment_request_data: payment_request.into(), + }, + )) }, }), ..item.data @@ -182,7 +184,7 @@ impl } } -impl From for payments::ApplePayRequest { +impl From for payments::ApplePayPaymentRequest { fn from(value: PaymentRequest) -> Self { Self { country_code: value.country_code, @@ -198,14 +200,14 @@ impl From for payments::AmountInfo { fn from(value: AmountInfo) -> Self { Self { label: value.label, - label_type: value.label_type, + total_type: value.label_type, amount: value.amount, } } } -impl From for payments::ApplePaySessionObject { - fn from(value: ApplepaySessionResponse) -> Self { +impl From for payments::ApplePaySessionResponse { + fn from(value: ApplepaySessionTokenResponse) -> Self { Self { epoch_timestamp: value.epoch_timestamp, expires_at: value.expires_at, diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs new file mode 100644 index 00000000000..1f81aff9f79 --- /dev/null +++ b/crates/router/src/connector/bluesnap.rs @@ -0,0 +1,744 @@ +mod transformers; + +use std::fmt::Debug; + +use base64::Engine; +use common_utils::crypto; +use error_stack::{IntoReport, ResultExt}; +use transformers as bluesnap; + +use super::utils::RefundsRequestData; +use crate::{ + configs::settings, + consts, + core::{ + errors::{self, CustomResult}, + payments, + }, + db::StorageInterface, + headers, logger, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Bluesnap; + +impl ConnectorCommonExt for Bluesnap +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut header = self.get_auth_header(&req.connector_auth_type)?; + header.push(( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string(), + )); + Ok(header) + } +} + +impl ConnectorCommon for Bluesnap { + fn id(&self) -> &'static str { + "bluesnap" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.bluesnap.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth: bluesnap::BluesnapAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let encoded_api_key = + consts::BASE64_ENGINE.encode(format!("{}:{}", auth.key1, auth.api_key)); + Ok(vec![( + headers::AUTHORIZATION.to_string(), + format!("Basic {encoded_api_key}"), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + logger::debug!(bluesnap_error_response=?res); + let response: bluesnap::BluesnapErrorResponse = res + .response + .parse_struct("BluesnapErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let response_error_message = response.message.first().map_or( + ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: None, + }, + |error_response| ErrorResponse { + status_code: res.status_code, + code: error_response.code.clone(), + message: error_response.description.clone(), + reason: None, + }, + ); + Ok(response_error_message) + } +} + +impl api::Payment for Bluesnap {} + +impl api::PreVerify for Bluesnap {} +impl ConnectorIntegration + for Bluesnap +{ +} + +impl api::PaymentVoid for Bluesnap {} + +impl ConnectorIntegration + for Bluesnap +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "services/2/transactions" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = bluesnap::BluesnapVoidRequest::try_from(req)?; + let bluesnap_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bluesnap_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Put) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: bluesnap::BluesnapPaymentsResponse = res + .response + .parse_struct("BluesnapPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::ConnectorAccessToken for Bluesnap {} + +impl ConnectorIntegration + for Bluesnap +{ +} + +impl api::PaymentSync for Bluesnap {} +impl ConnectorIntegration + for Bluesnap +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "services/2/transactions/", + connector_payment_id + )) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: bluesnap::BluesnapPaymentsResponse = res + .response + .parse_struct("BluesnapPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } +} + +impl api::PaymentCapture for Bluesnap {} +impl ConnectorIntegration + for Bluesnap +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "services/2/transactions" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = bluesnap::BluesnapCaptureRequest::try_from(req)?; + let bluesnap_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bluesnap_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Put) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: bluesnap::BluesnapPaymentsResponse = res + .response + .parse_struct("Bluesnap BluesnapPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: String = res + .response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(bluesnap_error_response=?res); + + Ok(ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: response, + reason: None, + }) + } +} + +impl api::PaymentSession for Bluesnap {} + +impl ConnectorIntegration + for Bluesnap +{ + //TODO: implement sessions flow +} + +impl api::PaymentAuthorize for Bluesnap {} + +impl ConnectorIntegration + for Bluesnap +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "services/2/transactions" + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(req)?; + let bluesnap_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bluesnap_req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: bluesnap::BluesnapPaymentsResponse = res + .response + .parse_struct("BluesnapPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::Refund for Bluesnap {} +impl api::RefundExecute for Bluesnap {} +impl api::RefundSync for Bluesnap {} + +impl ConnectorIntegration + for Bluesnap +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "services/2/transactions/refund/", + req.request.connector_transaction_id + )) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = bluesnap::BluesnapRefundRequest::try_from(req)?; + let bluesnap_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(bluesnap_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: bluesnap::RefundResponse = res + .response + .parse_struct("bluesnap RefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Bluesnap { + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "services/2/transactions/", + req.request.get_connector_refund_id()? + )) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: bluesnap::BluesnapPaymentsResponse = res + .response + .parse_struct("bluesnap BluesnapPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Bluesnap { + fn get_webhook_source_verification_algorithm( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::Md5)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let webhook_body: bluesnap::BluesnapWebhookBody = + serde_urlencoded::from_bytes(request.body) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + let signature = webhook_body.auth_key; + hex::decode(signature) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound) + } + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _secret: &[u8], + ) -> CustomResult, errors::ConnectorError> { + let webhook_body: bluesnap::BluesnapWebhookBody = + serde_urlencoded::from_bytes(request.body) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + let msg = webhook_body.reference_number + &webhook_body.contract_id; + Ok(msg.into_bytes()) + } + + async fn get_webhook_source_verification_merchant_secret( + &self, + db: &dyn StorageInterface, + merchant_id: &str, + ) -> CustomResult, errors::ConnectorError> { + let key = format!("whsec_verification_{}_{}", self.id(), merchant_id); + let secret = db + .find_config_by_key(&key) + .await + .change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?; + + Ok(secret.config.into_bytes()) + } + + async fn verify_webhook_source( + &self, + db: &dyn StorageInterface, + request: &api::IncomingWebhookRequestDetails<'_>, + merchant_id: &str, + ) -> CustomResult { + let algorithm = self + .get_webhook_source_verification_algorithm(request) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let signature = self + .get_webhook_source_verification_signature(request) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let mut secret = self + .get_webhook_source_verification_merchant_secret(db, merchant_id) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let mut message = self + .get_webhook_source_verification_message(request, merchant_id, &secret) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + message.append(&mut secret); + algorithm + .verify_signature(&secret, &signature, &message) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let webhook_body: bluesnap::BluesnapWebhookBody = + serde_urlencoded::from_bytes(request.body) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + Ok(webhook_body.reference_number) + } + + fn get_webhook_event_type( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let details: bluesnap::BluesnapWebhookObjectEventType = + serde_urlencoded::from_bytes(request.body) + .into_report() + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + + Ok(match details.transaction_type.as_str() { + "DECLINE" | "CC_CHARGE_FAILED" => api::IncomingWebhookEvent::PaymentIntentFailure, + "CHARGE" => api::IncomingWebhookEvent::PaymentIntentSuccess, + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound).into_report()?, + }) + } + + fn get_webhook_resource_object( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let details: bluesnap::BluesnapWebhookObjectResource = + serde_urlencoded::from_bytes(request.body) + .into_report() + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + let res_json = + utils::Encode::::encode_to_value(&details) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(res_json) + } +} + +impl services::ConnectorRedirectResponse for Bluesnap { + fn get_flow_type( + &self, + _query_params: &str, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs new file mode 100644 index 00000000000..89c088e1c7f --- /dev/null +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -0,0 +1,332 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + core::errors, + pii::{self, Secret}, + types::{ + self, api, + storage::enums, + transformers::{self, ForeignTryFrom}, + }, +}; + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapPaymentsRequest { + amount: i64, + #[serde(flatten)] + payment_method: PaymentMethodDetails, + currency: enums::Currency, + soft_descriptor: Option, + card_transaction_type: BluesnapTxnType, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum PaymentMethodDetails { + CreditCard(Card), +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Card { + card_number: Secret, + expiration_month: Secret, + expiration_year: Secret, + security_code: Secret, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let auth_mode = match item.request.capture_method { + Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly, + _ => BluesnapTxnType::AuthCapture, + }; + let payment_method = match item.request.payment_method_data.clone() { + api::PaymentMethod::Card(ccard) => Ok(PaymentMethodDetails::CreditCard(Card { + card_number: ccard.card_number, + expiration_month: ccard.card_exp_month.clone(), + expiration_year: ccard.card_exp_year.clone(), + security_code: ccard.card_cvc, + })), + _ => Err(errors::ConnectorError::NotImplemented( + "payment method".to_string(), + )), + }?; + Ok(Self { + amount: item.request.amount, + payment_method, + currency: item.request.currency, + soft_descriptor: item.description.clone(), + card_transaction_type: auth_mode, + }) + } +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapVoidRequest { + card_transaction_type: BluesnapTxnType, + transaction_id: String, +} + +impl TryFrom<&types::PaymentsCancelRouterData> for BluesnapVoidRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let card_transaction_type = BluesnapTxnType::AuthReversal; + let transaction_id = item.request.connector_transaction_id.to_string(); + Ok(Self { + card_transaction_type, + transaction_id, + }) + } +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapCaptureRequest { + card_transaction_type: BluesnapTxnType, + transaction_id: String, + amount: Option, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for BluesnapCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let card_transaction_type = BluesnapTxnType::Capture; + let transaction_id = item.request.connector_transaction_id.to_string(); + Ok(Self { + card_transaction_type, + transaction_id, + amount: item.request.amount_to_capture, + }) + } +} + +// Auth Struct +pub struct BluesnapAuthType { + pub(super) api_key: String, + pub(super) key1: String, +} + +impl TryFrom<&types::ConnectorAuthType> for BluesnapAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::BodyKey { api_key, key1 } = auth_type { + Ok(Self { + api_key: api_key.to_string(), + key1: key1.to_string(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType.into()) + } + } +} +// PaymentsResponse +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BluesnapTxnType { + AuthOnly, + AuthCapture, + AuthReversal, + Capture, + Refund, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum BluesnapProcessingStatus { + #[serde(alias = "success")] + Success, + #[default] + #[serde(alias = "pending")] + Pending, + #[serde(alias = "fail")] + Fail, + #[serde(alias = "pending_merchant_review")] + PendingMerchantReview, +} + +impl TryFrom> + for transformers::Foreign +{ + type Error = error_stack::Report; + fn try_from( + item: transformers::Foreign<(BluesnapTxnType, BluesnapProcessingStatus)>, + ) -> Result { + let item = item.0; + let (item_txn_status, item_processing_status) = item; + Ok(match item_processing_status { + BluesnapProcessingStatus::Success => match item_txn_status { + BluesnapTxnType::AuthOnly => enums::AttemptStatus::Authorized, + BluesnapTxnType::AuthReversal => enums::AttemptStatus::Voided, + BluesnapTxnType::AuthCapture | BluesnapTxnType::Capture => { + enums::AttemptStatus::Charged + } + BluesnapTxnType::Refund => enums::AttemptStatus::Charged, + }, + BluesnapProcessingStatus::Pending | BluesnapProcessingStatus::PendingMerchantReview => { + enums::AttemptStatus::Pending + } + BluesnapProcessingStatus::Fail => enums::AttemptStatus::Failure, + } + .into()) + } +} + +impl From for enums::RefundStatus { + fn from(item: BluesnapProcessingStatus) -> Self { + match item { + BluesnapProcessingStatus::Success => Self::Success, + BluesnapProcessingStatus::Pending => Self::Pending, + BluesnapProcessingStatus::PendingMerchantReview => Self::ManualReview, + BluesnapProcessingStatus::Fail => Self::Failure, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapPaymentsResponse { + processing_info: ProcessingInfoResponse, + transaction_id: String, + card_transaction_type: BluesnapTxnType, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Refund { + refund_transaction_id: String, + amount: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ProcessingInfoResponse { + processing_status: BluesnapProcessingStatus, + authorization_code: Option, + network_transaction_id: Option, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BluesnapPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::foreign_try_from(( + item.response.card_transaction_type, + item.response.processing_info.processing_status, + ))?, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Eq, PartialEq, Serialize)] +pub struct BluesnapRefundRequest { + amount: Option, + reason: Option, +} + +impl TryFrom<&types::RefundsRouterData> for BluesnapRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + Ok(Self { + reason: item.request.reason.clone(), + amount: Some(item.request.refund_amount), + }) + } +} + +#[derive(Default, Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RefundResponse { + refund_transaction_id: i32, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.transaction_id.clone(), + refund_status: enums::RefundStatus::from( + item.response.processing_info.processing_status, + ), + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.refund_transaction_id.to_string(), + refund_status: enums::RefundStatus::Pending, + }), + ..item.data + }) + } +} +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapWebhookBody { + pub auth_key: String, + pub contract_id: String, + pub reference_number: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapWebhookObjectEventType { + pub transaction_type: String, +} +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapWebhookObjectResource { + pub auth_key: String, + pub contract_id: String, + pub reference_number: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ErrorDetails { + pub code: String, + pub description: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapErrorResponse { + pub message: Vec, +} diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index cd116779de3..59e854f46d2 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -244,9 +244,11 @@ impl ) -> Result { Ok(Self { response: Ok(types::PaymentsResponseData::SessionResponse { - session_token: types::api::SessionToken::Paypal(Box::new(payments::PaypalData { - session_token: item.response.client_token.value, - })), + session_token: types::api::SessionToken::Paypal(Box::new( + payments::PaypalSessionTokenResponse { + session_token: item.response.client_token.value, + }, + )), }), ..item.data }) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 6dfe630ccfe..59ba3a28314 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -4,7 +4,7 @@ use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, AddressDetailsData, PaymentsRequestData, PhoneDetailsData}, + connector::utils::{self, AddressDetailsData, PhoneDetailsData, RouterData}, consts, core::errors, pii::PeekInterface, diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index b494d98f259..3ec3e50e2bd 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - connector::utils::{AddressDetailsData, PaymentsRequestData}, + connector::utils::{AddressDetailsData, RouterData}, core::errors, services, types::{self, api, storage::enums}, diff --git a/crates/router/src/connector/globalpay/transformers.rs b/crates/router/src/connector/globalpay/transformers.rs index 258d066629e..6973906f9e6 100644 --- a/crates/router/src/connector/globalpay/transformers.rs +++ b/crates/router/src/connector/globalpay/transformers.rs @@ -8,28 +8,25 @@ use super::{ response::{GlobalpayPaymentStatus, GlobalpayPaymentsResponse, GlobalpayRefreshTokenResponse}, }; use crate::{ - connector::utils::{self, CardData, PaymentsRequestData}, + connector::utils::{CardData, PaymentsRequestData, RouterData}, consts, core::errors, types::{self, api, storage::enums, ErrorResponse}, }; +#[derive(Debug, Serialize, Deserialize)] +pub struct GlobalPayMeta { + account_name: String, +} + impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobalpayPaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { - let metadata = item - .connector_meta_data - .to_owned() - .ok_or_else(utils::missing_field_err("connector_meta"))?; - let account_name = metadata - .as_object() - .and_then(|o| o.get("account_name")) - .map(|o| o.to_string()) - .ok_or_else(utils::missing_field_err("connector_meta.account_name"))?; + let metadata: GlobalPayMeta = item.to_connector_meta()?; let card = item.get_card()?; let expiry_year = card.get_card_expiry_year_2_digit(); Ok(Self { - account_name, + account_name: metadata.account_name, amount: Some(item.request.amount.to_string()), currency: item.request.currency.to_string(), reference: item.attempt_id.to_string(), diff --git a/crates/router/src/connector/klarna/transformers.rs b/crates/router/src/connector/klarna/transformers.rs index 2d4a02289b7..e065014efb0 100644 --- a/crates/router/src/connector/klarna/transformers.rs +++ b/crates/router/src/connector/klarna/transformers.rs @@ -72,10 +72,12 @@ impl TryFrom> let response = &item.response; Ok(Self { response: Ok(types::PaymentsResponseData::SessionResponse { - session_token: types::api::SessionToken::Klarna(Box::new(payments::KlarnaData { - session_token: response.client_token.clone(), - session_id: response.session_id.clone(), - })), + session_token: types::api::SessionToken::Klarna(Box::new( + payments::KlarnaSessionTokenResponse { + session_token: response.client_token.clone(), + session_id: response.session_id.clone(), + }, + )), }), ..item.data }) diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs new file mode 100644 index 00000000000..9aff6524cf5 --- /dev/null +++ b/crates/router/src/connector/nuvei.rs @@ -0,0 +1,670 @@ +mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use transformers as nuvei; + +use crate::{ + configs::settings, + core::{ + errors::{self, CustomResult}, + payments, + }, + headers, logger, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, RouterData, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Nuvei; + +impl ConnectorCommonExt for Nuvei +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + _req: &RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let headers = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string(), + )]; + Ok(headers) + } +} + +impl ConnectorCommon for Nuvei { + fn id(&self) -> &'static str { + "nuvei" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.nuvei.base_url.as_ref() + } + + fn get_auth_header( + &self, + _auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + Ok(vec![]) + } +} + +impl api::Payment for Nuvei {} + +impl api::PreVerify for Nuvei {} +impl ConnectorIntegration + for Nuvei +{ +} + +impl api::PaymentVoid for Nuvei {} + +impl ConnectorIntegration + for Nuvei +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/voidTransaction.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + let req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(target: "router::connector::nuvei", response=?res); + let response: nuvei::NuveiPaymentsResponse = res + .response + .parse_struct("nuvei NuveiPaymentsResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::PaymentsCancelRouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::ConnectorAccessToken for Nuvei {} + +impl ConnectorIntegration + for Nuvei +{ +} + +impl api::PaymentSync for Nuvei {} +impl ConnectorIntegration + for Nuvei +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/getPaymentStatus.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsSyncRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = nuvei::NuveiPaymentSyncRequest::try_from(req)?; + let req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .body(types::PaymentsSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + logger::debug!(payment_sync_response=?res); + let response: nuvei::NuveiPaymentsResponse = res + .response + .parse_struct("nuvei PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } +} + +impl api::PaymentCapture for Nuvei {} +impl ConnectorIntegration + for Nuvei +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/settleTransaction.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + let req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: nuvei::NuveiPaymentsResponse = res + .response + .parse_struct("Nuvei PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(nuveipayments_create_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::PaymentSession for Nuvei {} + +impl ConnectorIntegration + for Nuvei +{ +} + +impl api::PaymentAuthorize for Nuvei {} + +#[async_trait::async_trait] +impl ConnectorIntegration + for Nuvei +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/payment.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + + async fn execute_pretasks( + &self, + router_data: &mut types::PaymentsAuthorizeRouterData, + app_state: &crate::routes::AppState, + ) -> CustomResult<(), errors::ConnectorError> { + let integ: Box< + &(dyn ConnectorIntegration< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + > + Send + + Sync + + 'static), + > = Box::new(&Self); + let authorize_data = &types::PaymentsAuthorizeSessionTokenRouterData::from(&router_data); + let resp = services::execute_connector_processing_step( + app_state, + integ, + authorize_data, + payments::CallConnectorAction::Trigger, + ) + .await?; + router_data.session_token = resp.session_token; + Ok(()) + } + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = nuvei::NuveiPaymentsRequest::try_from(req)?; + let req = utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: nuvei::NuveiPaymentsResponse = res + .response + .parse_struct("nuvei NuveiPaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(nuveipayments_create_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + > for Nuvei +{ + fn get_headers( + &self, + req: &RouterData< + types::api::payments::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &RouterData< + types::api::payments::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/getSessionToken.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_request_body( + &self, + req: &RouterData< + types::api::payments::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = nuvei::NuveiSessionRequest::try_from(req)?; + let req = utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + + fn build_request( + &self, + req: &RouterData< + types::api::payments::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPreAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsPreAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsPreAuthorizeType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RouterData< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + res: Response, + ) -> CustomResult< + RouterData< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > + where + api::AuthorizeSessionToken: Clone, + types::AuthorizeSessionTokenData: Clone, + types::PaymentsResponseData: Clone, + { + let response: nuvei::NuveiSessionResponse = res + .response + .parse_struct("nuvei NuveiSessionResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + logger::debug!(nuvei_session_response=?response); + + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::Refund for Nuvei {} +impl api::RefundExecute for Nuvei {} +impl api::RefundSync for Nuvei {} + +impl ConnectorIntegration for Nuvei { + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}ppp/api/v1/refundTransaction.do", + api::ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + let req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + logger::debug!(target: "router::connector::nuvei", response=?res); + let response: nuvei::NuveiPaymentsResponse = res + .response + .parse_struct("nuvei NuveiPaymentsResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Nuvei {} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Nuvei { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} + +impl services::ConnectorRedirectResponse for Nuvei { + fn get_flow_type( + &self, + _query_params: &str, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs new file mode 100644 index 00000000000..6fa32954a90 --- /dev/null +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -0,0 +1,615 @@ +use common_utils::{ + crypto::{self, GenerateDigest}, + date_time, +}; +use error_stack::{IntoReport, ResultExt}; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::{PaymentsCancelRequestData, RouterData}, + core::errors, + types::{self, api, storage::enums}, +}; + +#[derive(Debug, Serialize, Default, Deserialize)] +pub struct NuveiMeta { + pub session_token: String, +} + +#[derive(Debug, Serialize, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiSessionRequest { + pub merchant_id: String, + pub merchant_site_id: String, + pub client_request_id: String, + pub time_stamp: String, + pub checksum: String, +} + +#[derive(Debug, Serialize, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiSessionResponse { + pub session_token: String, + pub internal_request_id: i64, + pub status: String, + pub err_code: i64, + pub reason: String, + pub merchant_id: String, + pub merchant_site_id: String, + pub version: String, + pub client_request_id: String, +} + +#[derive(Debug, Serialize, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiPaymentsRequest { + pub time_stamp: String, + pub session_token: String, + pub merchant_id: String, + pub merchant_site_id: String, + pub client_request_id: String, + pub amount: String, + pub currency: String, + pub user_token_id: String, + pub client_unique_id: String, + pub transaction_type: TransactionType, + pub payment_option: PaymentOption, + pub checksum: String, +} + +#[derive(Debug, Serialize, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiPaymentFlowRequest { + pub time_stamp: String, + pub merchant_id: String, + pub merchant_site_id: String, + pub client_request_id: String, + pub amount: String, + pub currency: String, + pub related_transaction_id: Option, + pub checksum: String, +} + +#[derive(Debug, Serialize, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiPaymentSyncRequest { + pub session_token: String, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub enum TransactionType { + Auth, + #[default] + Sale, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentOption { + pub card: Card, + pub user_payment_option_id: Option, + pub device_details: Option, + pub billing_address: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct BillingAddress { + pub email: String, + pub country: String, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Card { + pub card_number: Option>, + pub card_holder_name: Option>, + pub expiration_month: Option>, + pub expiration_year: Option>, + #[serde(rename = "CVV")] + pub cvv: Option>, + pub three_d: Option, + pub cc_card_number: Option, + pub bin: Option, + pub last4_digits: Option, + pub cc_exp_month: Option, + pub cc_exp_year: Option, + pub acquirer_id: Option, + pub cvv2_reply: Option, + pub avs_code: Option, + pub card_type: Option, + pub card_brand: Option, + pub issuer_bank_name: Option, + pub issuer_country: Option, + pub is_prepaid: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreeD { + pub browser_details: Option, + pub version: Option, + pub notification_url: Option, + pub merchant_url: Option, + pub platform_type: Option, + pub v2_additional_params: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BrowserDetails { + pub accept_header: String, + pub ip: String, + pub java_enabled: String, + pub java_script_enabled: String, + pub language: String, + pub color_depth: String, + pub screen_height: String, + pub screen_width: String, + pub time_zone: String, + pub user_agent: String, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct V2AdditionalParams { + pub challenge_window_size: String, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeviceDetails { + pub ip_address: String, +} + +impl From for TransactionType { + fn from(value: enums::CaptureMethod) -> Self { + match value { + enums::CaptureMethod::Manual => Self::Auth, + _ => Self::Sale, + } + } +} + +fn encode_payload( + payload: Vec, +) -> Result> { + let data = payload.join(""); + let digest = crypto::Sha256 + .generate_digest(data.as_bytes()) + .change_context(errors::ConnectorError::RequestEncodingFailed) + .attach_printable("error encoding the payload")?; + Ok(hex::encode(digest)) +} + +impl TryFrom<&types::PaymentsAuthorizeSessionTokenRouterData> for NuveiSessionRequest { + type Error = error_stack::Report; + fn try_from( + item: &types::PaymentsAuthorizeSessionTokenRouterData, + ) -> Result { + let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?; + let merchant_id = connector_meta.merchant_id; + let merchant_site_id = connector_meta.merchant_site_id; + let client_request_id = item.attempt_id.clone(); + let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let merchant_secret = connector_meta.merchant_secret; + Ok(Self { + merchant_id: merchant_id.clone(), + merchant_site_id: merchant_site_id.clone(), + client_request_id: client_request_id.clone(), + time_stamp: time_stamp.clone(), + checksum: encode_payload(vec![ + merchant_id, + merchant_site_id, + client_request_id, + time_stamp, + merchant_secret, + ])?, + }) + } +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::Pending, + session_token: Some(item.response.session_token.clone()), + response: Ok(types::PaymentsResponseData::SessionTokenResponse { + session_token: item.response.session_token, + }), + ..item.data + }) + } +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for NuveiPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?; + let merchant_id = connector_meta.merchant_id; + let merchant_site_id = connector_meta.merchant_site_id; + let client_request_id = item.attempt_id.clone(); + let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let merchant_secret = connector_meta.merchant_secret; + match item.request.payment_method_data.clone() { + api::PaymentMethod::Card(card) => Ok(Self { + merchant_id: merchant_id.clone(), + merchant_site_id: merchant_site_id.clone(), + client_request_id: client_request_id.clone(), + amount: item.request.amount.clone().to_string(), + currency: item.request.currency.clone().to_string(), + transaction_type: item + .request + .capture_method + .map(TransactionType::from) + .unwrap_or_default(), + payment_option: PaymentOption { + card: Card { + card_number: Some(card.card_number), + card_holder_name: Some(card.card_holder_name), + expiration_month: Some(card.card_exp_month), + expiration_year: Some(card.card_exp_year), + cvv: Some(card.card_cvc), + ..Default::default() + }, + ..Default::default() + }, + time_stamp: time_stamp.clone(), + session_token: item.get_session_token()?, + checksum: encode_payload(vec![ + merchant_id, + merchant_site_id, + client_request_id, + item.request.amount.to_string(), + item.request.currency.to_string(), + time_stamp, + merchant_secret, + ])?, + ..Default::default() + }), + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for NuveiPaymentFlowRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?; + let merchant_id = connector_meta.merchant_id; + let merchant_site_id = connector_meta.merchant_site_id; + let client_request_id = item.attempt_id.clone(); + let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let merchant_secret = connector_meta.merchant_secret; + Ok(Self { + merchant_id: merchant_id.clone(), + merchant_site_id: merchant_site_id.clone(), + client_request_id: client_request_id.clone(), + amount: item.request.amount.clone().to_string(), + currency: item.request.currency.clone().to_string(), + related_transaction_id: Some(item.request.connector_transaction_id.clone()), + time_stamp: time_stamp.clone(), + checksum: encode_payload(vec![ + merchant_id, + merchant_site_id, + client_request_id, + item.request.amount.to_string(), + item.request.currency.to_string(), + item.request.connector_transaction_id.clone(), + time_stamp, + merchant_secret, + ])?, + }) + } +} + +impl TryFrom<&types::RefundExecuteRouterData> for NuveiPaymentFlowRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundExecuteRouterData) -> Result { + let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?; + let merchant_id = connector_meta.merchant_id; + let merchant_site_id = connector_meta.merchant_site_id; + let client_request_id = item.attempt_id.clone(); + let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let merchant_secret = connector_meta.merchant_secret; + Ok(Self { + merchant_id: merchant_id.clone(), + merchant_site_id: merchant_site_id.clone(), + client_request_id: client_request_id.clone(), + amount: item.request.amount.clone().to_string(), + currency: item.request.currency.clone().to_string(), + related_transaction_id: Some(item.request.connector_transaction_id.clone()), + time_stamp: time_stamp.clone(), + checksum: encode_payload(vec![ + merchant_id, + merchant_site_id, + client_request_id, + item.request.amount.to_string(), + item.request.currency.to_string(), + item.request.connector_transaction_id.clone(), + time_stamp, + merchant_secret, + ])?, + }) + } +} + +impl TryFrom<&types::PaymentsSyncRouterData> for NuveiPaymentSyncRequest { + type Error = error_stack::Report; + fn try_from(value: &types::PaymentsSyncRouterData) -> Result { + let meta: NuveiMeta = value.to_connector_meta()?; + Ok(Self { + session_token: meta.session_token, + }) + } +} + +impl TryFrom<&types::PaymentsCancelRouterData> for NuveiPaymentFlowRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let connector_meta: NuveiAuthType = NuveiAuthType::try_from(&item.connector_auth_type)?; + let merchant_id = connector_meta.merchant_id; + let merchant_site_id = connector_meta.merchant_site_id; + let client_request_id = item.attempt_id.clone(); + let time_stamp = date_time::date_as_yyyymmddhhmmss(); + let merchant_secret = connector_meta.merchant_secret; + let amount = item.request.get_amount()?.to_string(); + let currency = item.request.get_currency()?.to_string(); + Ok(Self { + merchant_id: merchant_id.clone(), + merchant_site_id: merchant_site_id.clone(), + client_request_id: client_request_id.clone(), + amount: amount.clone(), + currency: currency.clone(), + related_transaction_id: Some(item.request.connector_transaction_id.clone()), + time_stamp: time_stamp.clone(), + checksum: encode_payload(vec![ + merchant_id, + merchant_site_id, + client_request_id, + amount, + currency, + item.request.connector_transaction_id.clone(), + time_stamp, + merchant_secret, + ])?, + }) + } +} + +// Auth Struct +pub struct NuveiAuthType { + pub(super) merchant_id: String, + pub(super) merchant_site_id: String, + pub(super) merchant_secret: String, +} + +impl TryFrom<&types::ConnectorAuthType> for NuveiAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } = auth_type + { + Ok(Self { + merchant_id: api_key.to_string(), + merchant_site_id: key1.to_string(), + merchant_secret: api_secret.to_string(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum NuveiPaymentStatus { + Success, + Failed, + Error, + #[default] + Processing, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum NuveiTransactionStatus { + Approved, + Declined, + Error, + #[default] + Processing, +} + +impl From for enums::AttemptStatus { + fn from(item: NuveiTransactionStatus) -> Self { + match item { + NuveiTransactionStatus::Approved => Self::Charged, + NuveiTransactionStatus::Declined | NuveiTransactionStatus::Error => Self::Failure, + _ => Self::Pending, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NuveiPaymentsResponse { + pub order_id: Option, + pub user_token_id: Option, + pub payment_option: Option, + pub transaction_status: Option, + pub gw_error_code: Option, + pub gw_extended_error_code: Option, + pub issuer_decline_code: Option, + pub issuer_decline_reason: Option, + pub transaction_type: Option, + pub transaction_id: Option, + pub external_transaction_id: Option, + pub auth_code: Option, + pub custom_data: Option, + pub fraud_details: Option, + pub external_scheme_transaction_id: Option, + pub session_token: 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, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum NuveiTransactionType { + Auth, + Sale, + Credit, + Settle, + Void, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FraudDetails { + pub final_decision: String, +} + +fn get_payment_status(response: &NuveiPaymentsResponse) -> enums::AttemptStatus { + match response.transaction_status.clone() { + Some(status) => match status { + NuveiTransactionStatus::Approved => match response.transaction_type { + Some(NuveiTransactionType::Auth) => enums::AttemptStatus::Authorized, + Some(NuveiTransactionType::Sale) | Some(NuveiTransactionType::Settle) => { + enums::AttemptStatus::Charged + } + Some(NuveiTransactionType::Void) => enums::AttemptStatus::Voided, + _ => enums::AttemptStatus::Pending, + }, + NuveiTransactionStatus::Declined | NuveiTransactionStatus::Error => { + match response.transaction_type { + Some(NuveiTransactionType::Auth) => enums::AttemptStatus::AuthorizationFailed, + Some(NuveiTransactionType::Sale) | Some(NuveiTransactionType::Settle) => { + enums::AttemptStatus::Failure + } + Some(NuveiTransactionType::Void) => enums::AttemptStatus::VoidFailed, + _ => enums::AttemptStatus::Pending, + } + } + NuveiTransactionStatus::Processing => enums::AttemptStatus::Pending, + }, + None => match response.status { + NuveiPaymentStatus::Failed | NuveiPaymentStatus::Error => enums::AttemptStatus::Failure, + _ => enums::AttemptStatus::Pending, + }, + } +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + status: get_payment_status(&item.response), + response: match item.response.status { + NuveiPaymentStatus::Error => Err(types::ErrorResponse { + code: item + .response + .err_code + .ok_or(errors::ParsingError)? + .to_string(), + message: item.response.reason.clone().ok_or(errors::ParsingError)?, + reason: None, + status_code: item.http_code, + }), + _ => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id.ok_or(errors::ParsingError)?, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: Some( + serde_json::to_value(NuveiMeta { + session_token: item.response.session_token.unwrap_or_default(), + }) + .into_report() + .change_context(errors::ParsingError)?, + ), + }), + }, + ..item.data + }) + } +} + +impl From for enums::RefundStatus { + fn from(item: NuveiTransactionStatus) -> Self { + match item { + NuveiTransactionStatus::Approved => Self::Success, + NuveiTransactionStatus::Declined | NuveiTransactionStatus::Error => Self::Failure, + NuveiTransactionStatus::Processing => Self::Pending, + } + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund_status = item + .response + .transaction_status + .map(|a| a.into()) + .unwrap_or(enums::RefundStatus::Failure); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.transaction_id.ok_or(errors::ParsingError)?, + refund_status, + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund_status = item + .response + .transaction_status + .map(|a| a.into()) + .unwrap_or(enums::RefundStatus::Failure); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.transaction_id.ok_or(errors::ParsingError)?, + refund_status, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct NuveiErrorResponse {} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 6428ceb55ae..4b3ff44906f 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -4,7 +4,7 @@ use masking::Secret; use crate::{ core::errors::{self, CustomResult}, pii::PeekInterface, - types::{self, api}, + types::{self, api, PaymentsCancelData}, utils::OptionExt, }; @@ -34,15 +34,37 @@ impl AccessTokenRequestInfo for types::RefreshTokenRouterData { } } -pub trait PaymentsRequestData { +pub trait RouterData { fn get_billing(&self) -> Result<&api::Address, Error>; fn get_billing_country(&self) -> Result; fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>; + fn get_connector_meta(&self) -> Result; + fn get_session_token(&self) -> Result; fn get_billing_address(&self) -> Result<&api::AddressDetails, Error>; + fn to_connector_meta(&self) -> Result + where + T: serde::de::DeserializeOwned; +} + +pub trait PaymentsRequestData { fn get_card(&self) -> Result; fn get_return_url(&self) -> Result; } +pub trait PaymentsCancelRequestData { + fn get_amount(&self) -> Result; + fn get_currency(&self) -> Result; +} + +impl PaymentsCancelRequestData for PaymentsCancelData { + fn get_amount(&self) -> Result { + self.amount.ok_or_else(missing_field_err("amount")) + } + fn get_currency(&self) -> Result { + self.currency.ok_or_else(missing_field_err("currency")) + } +} + pub trait RefundsRequestData { fn get_connector_refund_id(&self) -> Result; } @@ -56,7 +78,7 @@ impl RefundsRequestData for types::RefundsData { } } -impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { +impl RouterData for types::RouterData { fn get_billing_country(&self) -> Result { self.address .billing @@ -66,13 +88,6 @@ impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { .ok_or_else(missing_field_err("billing.address.country")) } - fn get_card(&self) -> Result { - match self.request.payment_method_data.clone() { - api::PaymentMethod::Card(card) => Ok(card), - _ => Err(missing_field_err("card")()), - } - } - fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error> { self.address .billing @@ -94,11 +109,40 @@ impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { .ok_or_else(missing_field_err("billing")) } + fn get_connector_meta(&self) -> Result { + self.connector_meta_data + .clone() + .ok_or_else(missing_field_err("connector_meta_data")) + } + + fn get_session_token(&self) -> Result { + self.session_token + .clone() + .ok_or_else(missing_field_err("session_token")) + } + + fn to_connector_meta(&self) -> Result + where + T: serde::de::DeserializeOwned, + { + serde_json::from_value::(self.get_connector_meta()?) + .into_report() + .change_context(errors::ConnectorError::NoConnectorMetaData) + } +} + +impl PaymentsRequestData for types::PaymentsAuthorizeRouterData { fn get_return_url(&self) -> Result { self.router_return_url .clone() .ok_or_else(missing_field_err("router_return_url")) } + fn get_card(&self) -> Result { + match self.request.payment_method_data.clone() { + api::PaymentMethod::Card(card) => Ok(card), + _ => Err(missing_field_err("card")()), + } + } } pub trait CardData { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index c1b59800f02..3e00620664d 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -63,7 +63,7 @@ pub async fn create_merchant_account( merchant_name: req.merchant_name, api_key, merchant_details, - return_url: req.return_url, + return_url: req.return_url.map(|a| a.to_string()), webhook_details, routing_algorithm: req.routing_algorithm, sub_merchants_enabled: req.sub_merchants_enabled, @@ -146,7 +146,7 @@ pub async fn merchant_account_update( .transpose() .change_context(errors::ApiErrorResponse::InternalServerError)?, - return_url: req.return_url, + return_url: req.return_url.map(|a| a.to_string()), webhook_details: req .webhook_details diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index f56d44552ec..57869ef9c06 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use api_models::{admin, enums as api_enums}; use common_utils::{consts, ext_traits::AsyncExt, generate_id}; @@ -382,7 +382,7 @@ pub async fn list_payment_methods( error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound) })?; - let mut response: HashSet = HashSet::new(); + let mut response: HashMap> = HashMap::new(); for mca in all_mcas { let payment_methods = match mca.payment_methods_enabled { Some(pm) => pm, @@ -396,6 +396,7 @@ pub async fn list_payment_methods( payment_intent.as_ref(), payment_attempt.as_ref(), address.as_ref(), + mca.connector_name, ) .await?; } @@ -406,7 +407,13 @@ pub async fn list_payment_methods( .unwrap_or(Ok(services::ApplicationResponse::Json( api::ListPaymentMethodResponse { redirect_url: merchant_account.return_url, - payment_methods: response, + payment_methods: response + .into_iter() + .map(|(mut key, val)| { + key.eligible_connectors = Some(val); + key + }) + .collect(), }, ))) } @@ -414,10 +421,11 @@ pub async fn list_payment_methods( async fn filter_payment_methods( payment_methods: Vec, req: &mut api::ListPaymentMethodRequest, - resp: &mut HashSet, + resp: &mut HashMap>, payment_intent: Option<&storage::PaymentIntent>, payment_attempt: Option<&storage::PaymentAttempt>, address: Option<&storage::Address>, + connector_name: String, ) -> errors::CustomResult<(), errors::ApiErrorResponse> { for payment_method in payment_methods.into_iter() { if let Ok(payment_method_object) = @@ -458,7 +466,9 @@ async fn filter_payment_methods( }; if filter && filter2 && filter3 { - resp.insert(payment_method_object); + resp.entry(payment_method_object) + .or_insert_with(Vec::new) + .push(connector_name.clone()); } } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5c65854ad8d..b7b0e982042 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -212,7 +212,7 @@ where FData: Send, Op: Operation + Send + Sync + Clone, Req: Debug, - Res: transformers::ToResponse, Op> + TryFrom, + Res: transformers::ToResponse, Op>, // To create connector flow specific interface data PaymentData: ConstructFlowSpecificData, types::RouterData: Feature, diff --git a/crates/router/src/core/payments/access_token.rs b/crates/router/src/core/payments/access_token.rs index a43a23bedd6..10487b2cf94 100644 --- a/crates/router/src/core/payments/access_token.rs +++ b/crates/router/src/core/payments/access_token.rs @@ -45,6 +45,8 @@ pub fn router_data_type_conversion( status: router_data.status, attempt_id: router_data.attempt_id, access_token: router_data.access_token, + session_token: router_data.session_token, + reference_id: None, } } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 909feb27d51..c60d1b734b6 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -46,7 +46,7 @@ impl #[async_trait] impl Feature for types::PaymentsAuthorizeRouterData { async fn decide_flows<'a>( - self, + mut self, state: &AppState, connector: &api::ConnectorData, customer: &Option, @@ -81,7 +81,7 @@ impl Feature for types::PaymentsAu impl types::PaymentsAuthorizeRouterData { pub async fn decide_flow<'a, 'b>( - &'b self, + &'b mut self, state: &'a AppState, connector: &api::ConnectorData, maybe_customer: &Option, @@ -97,6 +97,10 @@ impl types::PaymentsAuthorizeRouterData { types::PaymentsAuthorizeData, types::PaymentsResponseData, > = connector.connector.get_connector_integration(); + connector_integration + .execute_pretasks(self, state) + .await + .map_err(|error| error.to_payment_failed_response())?; let resp = services::execute_connector_processing_step( state, connector_integration, diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index b2950004dbc..785e5d08c33 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -91,11 +91,13 @@ fn create_gpay_session_token( let response_router_data = types::PaymentsSessionRouterData { response: Ok(types::PaymentsResponseData::SessionResponse { - session_token: payment_types::SessionToken::Gpay(Box::new(payment_types::GpayData { - merchant_info: gpay_data.data.merchant_info, - allowed_payment_methods: gpay_data.data.allowed_payment_methods, - transaction_info, - })), + session_token: payment_types::SessionToken::Gpay(Box::new( + payment_types::GpaySessionTokenResponse { + merchant_info: gpay_data.data.merchant_info, + allowed_payment_methods: gpay_data.data.allowed_payment_methods, + transaction_info, + }, + )), }), ..router_data.clone() }; diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index cca49294759..9ff2f81b783 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -161,7 +161,7 @@ impl GetTracker, api::PaymentsRequest> for Pa payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request.return_url.clone(); + payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); Ok(( Box::new(self), @@ -262,7 +262,10 @@ impl Domain for PaymentConfirm { ) -> CustomResult { // Use a new connector in the confirm call or use the same one which was passed when // creating the payment or if none is passed then use the routing algorithm - let request_connector = request.connector.map(|connector| connector.to_string()); + let request_connector = request + .connector + .as_ref() + .and_then(|connector| connector.first().map(|c| c.to_string())); helpers::get_connector_default( state, request_connector.as_ref().or(previously_used_connector), diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index a297cdaeda7..973f3921075 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -259,7 +259,10 @@ impl Domain for PaymentCreate { request: &api::PaymentsRequest, _previously_used_connector: Option<&String>, ) -> CustomResult { - let request_connector = request.connector.map(|connector| connector.to_string()); + let request_connector = request + .connector + .as_ref() + .and_then(|connector| connector.first().map(|c| c.to_string())); helpers::get_connector_default(state, request_connector.as_ref()).await } } @@ -474,7 +477,7 @@ impl PaymentCreate { client_secret: Some(client_secret), setup_future_usage: request.setup_future_usage.map(ForeignInto::foreign_into), off_session: request.off_session, - return_url: request.return_url.clone(), + return_url: request.return_url.as_ref().map(|a| a.to_string()), shipping_address_id, billing_address_id, statement_descriptor_name: request.statement_descriptor_name.clone(), diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 5dc58a076d3..1236b71e245 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -330,6 +330,7 @@ async fn payment_response_update_tracker( } types::PaymentsResponseData::SessionResponse { .. } => (None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), }, }; diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index e013c91a3fd..5447e721719 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -140,7 +140,7 @@ impl GetTracker, api::PaymentsRequest> for Pa payment_intent.shipping_address_id = shipping_address.clone().map(|x| x.address_id); payment_intent.billing_address_id = billing_address.clone().map(|x| x.address_id); - payment_intent.return_url = request.return_url.clone(); + payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); let token = token.or_else(|| payment_attempt.payment_token.clone()); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index a5a35ffeb1b..81dd61d55ff 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -98,6 +98,8 @@ where response: response.map_or_else(|| Err(types::ErrorResponse::default()), Ok), amount_captured: payment_data.payment_intent.amount_captured, access_token: None, + session_token: None, + reference_id: None, }; Ok(router_data) @@ -105,7 +107,7 @@ where pub trait ToResponse where - Self: TryFrom, + Self: Sized, Op: Debug, { fn generate_response( @@ -120,7 +122,6 @@ where impl ToResponse, Op> for api::PaymentsResponse where - Self: TryFrom, F: Clone, Op: Debug, { @@ -235,7 +236,6 @@ pub fn payments_to_payments_response( operation: Op, ) -> RouterResponse where - api::PaymentsResponse: TryFrom, Op: Debug, { let currency = payment_attempt @@ -251,16 +251,13 @@ where }; Ok(match payment_request { - Some(request) => { + Some(_request) => { if payments::is_start_pay(&operation) && redirection_data.is_some() { let redirection_data = redirection_data.get_required_value("redirection_data")?; let form: RedirectForm = serde_json::from_value(redirection_data) .map_err(|_| errors::ApiErrorResponse::InternalServerError)?; services::ApplicationResponse::Form(form) } else { - let mut response: api::PaymentsResponse = request - .try_into() - .map_err(|_| errors::ApiErrorResponse::InternalServerError)?; let mut next_action_response = None; if payment_intent.status == enums::IntentStatus::RequiresCustomerAction { next_action_response = Some(api::NextAction { @@ -272,7 +269,7 @@ where )), }) } - + let mut response: api::PaymentsResponse = Default::default(); services::ApplicationResponse::Json( response .set_payment_id(Some(payment_attempt.payment_id)) @@ -319,7 +316,6 @@ where .set_error_code(payment_attempt.error_code) .set_shipping(address.shipping) .set_billing(address.billing) - .to_owned() .set_next_action(next_action_response) .set_return_url(payment_intent.return_url) .set_cancellation_reason(payment_attempt.cancellation_reason) @@ -340,6 +336,7 @@ where .capture_method .map(ForeignInto::foreign_into), ) + .set_metadata(payment_intent.metadata) .to_owned(), ) } @@ -380,6 +377,7 @@ where billing: address.billing, cancellation_reason: payment_attempt.cancellation_reason, payment_token: payment_attempt.payment_token, + metadata: payment_intent.metadata, ..Default::default() }), }) @@ -474,6 +472,8 @@ impl TryFrom> for types::PaymentsCancelData { fn try_from(payment_data: PaymentData) -> Result { Ok(Self { + amount: Some(payment_data.amount.into()), + currency: Some(payment_data.currency), connector_transaction_id: payment_data .payment_attempt .connector_transaction_id diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index fccd7284589..fb072c80bfb 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -82,6 +82,8 @@ pub async fn construct_refund_router_data<'a, F>( refund_status: refund.refund_status, }), access_token: None, + session_token: None, + reference_id: None, }; Ok(router_data) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 9be89d49bfd..b38997e3465 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -173,8 +173,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentsSessionRequest, api_models::payments::PaymentsSessionResponse, api_models::payments::SessionToken, - api_models::payments::ApplePaySessionObject, - api_models::payments::ApplePayRequest, + api_models::payments::ApplePaySessionResponse, + api_models::payments::ApplePayPaymentRequest, api_models::payments::AmountInfo, api_models::payments::GpayMerchantInfo, api_models::payments::GpayAllowedPaymentMethods, @@ -182,10 +182,10 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::GpayTokenizationSpecification, api_models::payments::GpayTokenParameters, api_models::payments::GpayTransactionInfo, - api_models::payments::GpayData, - api_models::payments::KlarnaData, - api_models::payments::PaypalData, - api_models::payments::ApplepayData, + api_models::payments::GpaySessionTokenResponse, + api_models::payments::KlarnaSessionTokenResponse, + api_models::payments::PaypalSessionTokenResponse, + api_models::payments::ApplepaySessionTokenResponse, api_models::payments::PaymentsCancelRequest, api_models::payments::PaymentListConstraints, api_models::payments::PaymentListResponse, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 1a92a6f6cfd..9962d0237d1 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -29,7 +29,8 @@ use crate::{ routes::{app::AppStateInfo, AppState}, services::authentication as auth, types::{ - self, api, + self, + api::{self}, storage::{self}, ErrorResponse, }, @@ -51,7 +52,8 @@ where } } -pub trait ConnectorIntegration: ConnectorIntegrationAny { +#[async_trait::async_trait] +pub trait ConnectorIntegration: ConnectorIntegrationAny + Sync { fn get_headers( &self, _req: &types::RouterData, @@ -84,6 +86,26 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny, + _app_state: &AppState, + ) -> CustomResult<(), errors::ConnectorError> { + Ok(()) + } + + /// This module can be called after executing a payment flow where a post-task needed + /// Eg: Some connectors require payment sync to happen immediately after the authorize call to complete the transaction, we can add that logic in this block + async fn execute_posttasks( + &self, + _router_data: &mut types::RouterData, + _app_state: &AppState, + ) -> CustomResult<(), errors::ConnectorError> { + Ok(()) + } + fn build_request( &self, _req: &types::RouterData, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index ef52823d9cd..93c68a4ff37 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -22,6 +22,8 @@ use crate::{core::errors, services}; pub type PaymentsAuthorizeRouterData = RouterData; +pub type PaymentsAuthorizeSessionTokenRouterData = + RouterData; pub type PaymentsSyncRouterData = RouterData; pub type PaymentsCaptureRouterData = RouterData; @@ -51,6 +53,11 @@ pub type RefundsResponseRouterData = pub type PaymentsAuthorizeType = dyn services::ConnectorIntegration; +pub type PaymentsPreAuthorizeType = dyn services::ConnectorIntegration< + api::AuthorizeSessionToken, + AuthorizeSessionTokenData, + PaymentsResponseData, +>; pub type PaymentsSyncType = dyn services::ConnectorIntegration; pub type PaymentsCaptureType = @@ -88,6 +95,8 @@ pub struct RouterData { pub connector_meta_data: Option, pub amount_captured: Option, pub access_token: Option, + pub session_token: Option, + pub reference_id: Option, /// Contains flow-specific data required to construct a request and send it to the connector. pub request: Request, @@ -127,6 +136,14 @@ pub struct PaymentsCaptureData { pub amount: i64, } +#[derive(Debug, Clone)] +pub struct AuthorizeSessionTokenData { + pub amount_to_capture: Option, + pub currency: storage_enums::Currency, + pub connector_transaction_id: String, + pub amount: i64, +} + #[derive(Debug, Clone)] pub struct PaymentsSyncData { //TODO : add fields based on the connector requirements @@ -135,8 +152,10 @@ pub struct PaymentsSyncData { pub capture_method: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct PaymentsCancelData { + pub amount: Option, + pub currency: Option, pub connector_transaction_id: String, pub cancellation_reason: Option, } @@ -190,6 +209,9 @@ pub enum PaymentsResponseData { SessionResponse { session_token: api::SessionToken, }, + SessionTokenResponse { + session_token: String, + }, } #[derive(Debug, Clone, Default)] @@ -372,3 +394,36 @@ impl Default for ErrorResponse { Self::from(errors::ApiErrorResponse::InternalServerError) } } + +impl From<&&mut PaymentsAuthorizeRouterData> for PaymentsAuthorizeSessionTokenRouterData { + fn from(data: &&mut PaymentsAuthorizeRouterData) -> Self { + Self { + flow: PhantomData, + request: AuthorizeSessionTokenData { + amount_to_capture: data.amount_captured, + currency: data.request.currency, + connector_transaction_id: data.payment_id.clone(), + amount: data.request.amount, + }, + merchant_id: data.merchant_id.clone(), + connector: data.connector.clone(), + attempt_id: data.attempt_id.clone(), + status: data.status, + payment_method: data.payment_method, + connector_auth_type: data.connector_auth_type.clone(), + description: data.description.clone(), + return_url: data.return_url.clone(), + router_return_url: data.router_return_url.clone(), + address: data.address.clone(), + auth_type: data.auth_type, + connector_meta_data: data.connector_meta_data.clone(), + amount_captured: data.amount_captured, + access_token: data.access_token.clone(), + response: data.response.clone(), + payment_method_id: data.payment_method_id.clone(), + payment_id: data.payment_id.clone(), + session_token: data.session_token.clone(), + reference_id: data.reference_id.clone(), + } + } +} diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index a69f3df64e6..98229ab864a 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -162,8 +162,10 @@ impl ConnectorData { match connector_name { "aci" => Ok(Box::new(&connector::Aci)), "adyen" => Ok(Box::new(&connector::Adyen)), + "airwallex" => Ok(Box::new(&connector::Airwallex)), "applepay" => Ok(Box::new(&connector::Applepay)), "authorizedotnet" => Ok(Box::new(&connector::Authorizedotnet)), + "bluesnap" => Ok(Box::new(&connector::Bluesnap)), "braintree" => Ok(Box::new(&connector::Braintree)), "checkout" => Ok(Box::new(&connector::Checkout)), "cybersource" => Ok(Box::new(&connector::Cybersource)), @@ -171,6 +173,7 @@ impl ConnectorData { "fiserv" => Ok(Box::new(&connector::Fiserv)), "globalpay" => Ok(Box::new(&connector::Globalpay)), "klarna" => Ok(Box::new(&connector::Klarna)), + "nuvei" => Ok(Box::new(&connector::Nuvei)), "payu" => Ok(Box::new(&connector::Payu)), "rapyd" => Ok(Box::new(&connector::Rapyd)), "shift4" => Ok(Box::new(&connector::Shift4)), diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 7e808c2e52e..e9320b71030 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -64,6 +64,9 @@ impl super::Router for PaymentsRequest {} // Core related api layer. #[derive(Debug, Clone)] pub struct Authorize; + +#[derive(Debug, Clone)] +pub struct AuthorizeSessionToken; #[derive(Debug, Clone)] pub struct Capture; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index b900c630f94..b80ed75eb6b 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -463,3 +463,9 @@ impl From> .into() } } + +impl From> for F { + fn from(status: F) -> Self { + Self(frunk::labelled_convert_from(status.0)) + } +} diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 2aec793239c..9ba5f195009 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -59,6 +59,8 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { connector_meta_data: None, amount_captured: None, access_token: None, + session_token: None, + reference_id: None, } } @@ -97,6 +99,8 @@ fn construct_refund_router_data() -> types::RefundsRouterData { connector_meta_data: None, amount_captured: None, access_token: None, + session_token: None, + reference_id: None, } } diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 632d8faf718..1d2fc73dec1 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -162,6 +162,8 @@ async fn should_void_authorized_payment() { enums::CaptureMethod::Manual, ), Some(types::PaymentsCancelData { + amount: None, + currency: None, connector_transaction_id: String::from(""), cancellation_reason: Some("requested_by_customer".to_string()), }), diff --git a/crates/router/tests/connectors/airwallex.rs b/crates/router/tests/connectors/airwallex.rs new file mode 100644 index 00000000000..e9d40b62fe3 --- /dev/null +++ b/crates/router/tests/connectors/airwallex.rs @@ -0,0 +1,519 @@ +use masking::Secret; +use router::types::{self, api, storage::enums, AccessToken}; + +use crate::{ + connector_auth, + utils::{self, Connector, ConnectorActions}, +}; + +#[derive(Clone, Copy)] +struct AirwallexTest; +impl ConnectorActions for AirwallexTest {} + +static CONNECTOR: AirwallexTest = AirwallexTest {}; + +impl Connector for AirwallexTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Airwallex; + types::api::ConnectorData { + connector: Box::new(&Airwallex), + connector_name: types::Connector::Airwallex, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .airwallex + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "airwallex".to_string() + } +} + +fn get_access_token() -> Option { + match CONNECTOR.get_auth_token() { + types::ConnectorAuthType::BodyKey { api_key, key1 } => Some(AccessToken { + token: api_key, + expires: key1.parse::().unwrap(), + }), + _ => None, + } +} +fn get_default_payment_info() -> Option { + Some(utils::PaymentInfo { + access_token: get_access_token(), + router_return_url: Some("https://google.com".to_string()), + ..Default::default() + }) +} +fn payment_method_details() -> Option { + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new("4035501000000008".to_string()), + card_exp_month: Secret::new("02".to_string()), + card_exp_year: Secret::new("2035".to_string()), + card_holder_name: Secret::new("John Doe".to_string()), + card_cvc: Secret::new("123".to_string()), + }), + capture_method: Some(storage_models::enums::CaptureMethod::Manual), + ..utils::PaymentAuthorizeType::default().0 + }) +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: Some(50), + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + amount: None, + currency: None, + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect card number. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_incorrect_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new("1234567891011".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Invalid card number".to_string(), + ); +} + +// Creates a payment with empty card number. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_empty_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new(String::from("")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!(x.message, "Invalid card number",); +} + +// Creates a payment with incorrect CVC. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Invalid card cvc".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Invalid expiry month".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "payment_method.card should not be expired".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "The PaymentIntent status SUCCEEDED is invalid for operation cancel." + ); +} + +// Captures a payment using invalid connector payment id. +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from( + "The requested endpoint does not exist [/api/v1/pa/payment_intents/123456789/capture]" + ) + ); +} + +// Refunds a payment with refund amount higher than payment amount. +// #[serial_test::serial] +#[actix_web::test] +#[ignore] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/authorizedotnet.rs b/crates/router/tests/connectors/authorizedotnet.rs index cc3d36df9db..cadc748be6f 100644 --- a/crates/router/tests/connectors/authorizedotnet.rs +++ b/crates/router/tests/connectors/authorizedotnet.rs @@ -59,6 +59,8 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { connector_meta_data: None, amount_captured: None, access_token: None, + session_token: None, + reference_id: None, } } @@ -96,6 +98,8 @@ fn construct_refund_router_data() -> types::RefundsRouterData { address: PaymentAddress::default(), amount_captured: None, access_token: None, + session_token: None, + reference_id: None, } } diff --git a/crates/router/tests/connectors/bluesnap.rs b/crates/router/tests/connectors/bluesnap.rs new file mode 100644 index 00000000000..f3d82fee687 --- /dev/null +++ b/crates/router/tests/connectors/bluesnap.rs @@ -0,0 +1,540 @@ +use masking::Secret; +use router::types::{self, api, storage::enums, ConnectorAuthType}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions}, +}; + +#[derive(Clone, Copy)] +struct BluesnapTest; +impl ConnectorActions for BluesnapTest {} +static CONNECTOR: BluesnapTest = BluesnapTest {}; +impl utils::Connector for BluesnapTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Bluesnap; + types::api::ConnectorData { + connector: Box::new(&Bluesnap), + connector_name: types::Connector::Bluesnap, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .bluesnap + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "bluesnap".to_string() + } +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(None, None) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(None, None, None) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + None, + Some(types::PaymentsCaptureData { + amount_to_capture: Some(50), + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(None, None) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + None, + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + None, + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund(None, None, None, None) + .await + .unwrap(); + let rsync_response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + rsync_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + None, + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + let rsync_response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + rsync_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund(None, None, None, None) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(None, None, None) + .await + .unwrap(); + let rsync_response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + rsync_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + let rsync_response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + rsync_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let transaction_id = utils::get_connector_transaction_id(authorize_response.response).unwrap(); + for _x in 0..2 { + tokio::time::sleep(std::time::Duration::from_secs(5)).await; // to avoid 404 error + let refund_response = CONNECTOR + .refund_payment( + transaction_id.clone(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + let rsync_response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + rsync_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); + } +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(None, None, None) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect card number. + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_incorrect_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new("1234567891011".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Order creation failure due to problematic input.".to_string(), + ); +} + +// Creates a payment with empty card number. + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_empty_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new(String::from("")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!( + x.message, + "Order creation failure due to problematic input.", + ); +} + +// Creates a payment with incorrect CVC. + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Order creation failure due to problematic input.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Order creation failure due to problematic input.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Order creation failure due to problematic input.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, None) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "Transaction AUTH_REVERSAL failed. Transaction has already been captured." + ); +} + +// Captures a payment using invalid connector payment id. + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, None) + .await + .unwrap(); + + capture_response + .response + .unwrap_err() + .message + .contains("is not authorized to view transaction"); +} + +// Refunds a payment with refund amount higher than payment amount. + +#[serial_test::serial] +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + None, + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount cannot be more than the refundable order amount.", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/checkout.rs b/crates/router/tests/connectors/checkout.rs index ac9bc1196c4..113e78b8471 100644 --- a/crates/router/tests/connectors/checkout.rs +++ b/crates/router/tests/connectors/checkout.rs @@ -56,6 +56,8 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { connector_meta_data: None, amount_captured: None, access_token: None, + session_token: None, + reference_id: None, } } @@ -93,6 +95,8 @@ fn construct_refund_router_data() -> types::RefundsRouterData { address: PaymentAddress::default(), amount_captured: None, access_token: None, + session_token: None, + reference_id: None, } } diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 21c0245fde5..63d14f0497f 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -6,11 +6,14 @@ pub(crate) struct ConnectorAuthentication { pub dlocal: Option, pub aci: Option, pub adyen: Option, + pub airwallex: Option, pub authorizedotnet: Option, + pub bluesnap: Option, pub checkout: Option, pub cybersource: Option, pub fiserv: Option, pub globalpay: Option, + pub nuvei: Option, pub payu: Option, pub rapyd: Option, pub shift4: Option, diff --git a/crates/router/tests/connectors/cybersource.rs b/crates/router/tests/connectors/cybersource.rs index f960e3ea2da..3174cb43cf4 100644 --- a/crates/router/tests/connectors/cybersource.rs +++ b/crates/router/tests/connectors/cybersource.rs @@ -140,6 +140,7 @@ async fn should_void_already_authorized_payment() { Some(types::PaymentsCancelData { connector_transaction_id: "".to_string(), cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/dlocal.rs b/crates/router/tests/connectors/dlocal.rs index 26631f2e4fb..5e82a791fd8 100644 --- a/crates/router/tests/connectors/dlocal.rs +++ b/crates/router/tests/connectors/dlocal.rs @@ -108,6 +108,7 @@ async fn should_void_authorized_payment() { Some(types::PaymentsCancelData { connector_transaction_id: String::from(""), cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() }), Some(get_payment_info()), ) @@ -458,6 +459,7 @@ pub fn get_payment_info() -> PaymentInfo { auth_type: None, access_token: None, router_return_url: None, + connector_meta_data: None, } } // Connector dependent test cases goes here diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 35b3a0a0e9f..05e23fced4e 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -2,13 +2,16 @@ mod aci; mod adyen; +mod airwallex; mod authorizedotnet; +mod bluesnap; mod checkout; mod connector_auth; mod cybersource; mod dlocal; mod fiserv; mod globalpay; +mod nuvei; mod payu; mod rapyd; mod shift4; diff --git a/crates/router/tests/connectors/nuvei.rs b/crates/router/tests/connectors/nuvei.rs new file mode 100644 index 00000000000..cf4c3c0aef9 --- /dev/null +++ b/crates/router/tests/connectors/nuvei.rs @@ -0,0 +1,414 @@ +use masking::Secret; +use router::types::{ + self, api, + storage::{self, enums}, +}; +use serde_json::json; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions, PaymentInfo}, +}; + +#[derive(Clone, Copy)] +struct NuveiTest; +impl ConnectorActions for NuveiTest {} +impl utils::Connector for NuveiTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Nuvei; + types::api::ConnectorData { + connector: Box::new(&Nuvei), + connector_name: types::Connector::Nuvei, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .nuvei + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "nuvei".to_string() + } +} + +static CONNECTOR: NuveiTest = NuveiTest {}; + +fn get_payment_data() -> Option { + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new(String::from("4000027891380961")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }) +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(get_payment_data(), None) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(get_payment_data(), None, None) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + get_payment_data(), + Some(types::PaymentsCaptureData { + amount_to_capture: Some(50), + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(get_payment_data(), None) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + Some(PaymentInfo { + connector_meta_data: Some(json!({ + "session_token": authorize_response.session_token.unwrap() + })), + ..Default::default() + }), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + get_payment_data(), + Some(types::PaymentsCancelData { + cancellation_reason: Some("requested_by_customer".to_string()), + amount: Some(100), + currency: Some(storage::enums::Currency::USD), + ..Default::default() + }), + None, + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund(get_payment_data(), None, None, None) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Failure, //Nuvei fails refund always + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + get_payment_data(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Failure, //Nuvei fails refund always + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), None) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), None) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + Some(PaymentInfo { + connector_meta_data: Some(json!({ + "session_token": authorize_response.session_token.unwrap() + })), + ..Default::default() + }), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(get_payment_data(), None, None) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Failure, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + get_payment_data(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Failure, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect card number. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new("1234567891011".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Missing or invalid CardData data. Invalid credit card number.".to_string(), + ); +} + +// Creates a payment with empty card number. +#[actix_web::test] +async fn should_fail_payment_for_empty_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new(String::from("")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!( + x.message, + "Missing or invalid CardData data. Missing card number.", + ); +} + +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "cardData.CVV is invalid".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Invalid expired date".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_succeed_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethod::Card(api::Card { + card_number: Secret::new(String::from("4000027891380961")), + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..get_payment_data().unwrap() + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(get_payment_data(), None) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment( + txn_id.unwrap(), + Some(types::PaymentsCancelData { + cancellation_reason: Some("requested_by_customer".to_string()), + amount: Some(100), + currency: Some(storage::enums::Currency::USD), + ..Default::default() + }), + None, + ) + .await + .unwrap(); + assert_eq!(void_response.status, enums::AttemptStatus::Voided); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, None) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("Invalid relatedTransactionId") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + get_payment_data(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Failure, + ); +} diff --git a/crates/router/tests/connectors/shift4.rs b/crates/router/tests/connectors/shift4.rs index 66f24aaf2f4..3d78855b63f 100644 --- a/crates/router/tests/connectors/shift4.rs +++ b/crates/router/tests/connectors/shift4.rs @@ -134,6 +134,7 @@ async fn should_void_authorized_payment() { Some(types::PaymentsCancelData { connector_transaction_id: "".to_string(), cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/stripe.rs b/crates/router/tests/connectors/stripe.rs index 04fa9166ad3..ed256218566 100644 --- a/crates/router/tests/connectors/stripe.rs +++ b/crates/router/tests/connectors/stripe.rs @@ -143,6 +143,7 @@ async fn should_void_already_authorized_payment() { Some(types::PaymentsCancelData { connector_transaction_id: "".to_string(), // this connector_transaction_id will be ignored and the transaction_id from payment authorize data will be used for void cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() }), None, ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 94b930534cb..d469eebb383 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -4,10 +4,11 @@ use async_trait::async_trait; use error_stack::Report; use masking::Secret; use router::{ + configs::settings::Settings, core::{errors, errors::ConnectorError, payments}, db::StorageImpl, routes, services, - types::{self, api, storage::enums, AccessToken, PaymentAddress}, + types::{self, api, storage::enums, AccessToken, PaymentAddress, RouterData}, }; use wiremock::{Mock, MockServer}; @@ -30,6 +31,7 @@ pub struct PaymentInfo { pub auth_type: Option, pub access_token: Option, pub router_return_url: Option, + pub connector_meta_data: Option, } #[async_trait] @@ -40,7 +42,7 @@ pub trait ConnectorActions: Connector { payment_info: Option, ) -> Result> { let integration = self.get_data().connector.get_connector_integration(); - let request = self.generate_data( + let mut request = self.generate_data( types::PaymentsAuthorizeData { confirm: true, capture_method: Some(storage_models::enums::CaptureMethod::Manual), @@ -48,6 +50,10 @@ pub trait ConnectorActions: Connector { }, payment_info, ); + let state = + routes::AppState::with_storage(Settings::new().unwrap(), StorageImpl::PostgresqlTest) + .await; + integration.execute_pretasks(&mut request, &state).await?; call_connector(request, integration).await } @@ -57,7 +63,7 @@ pub trait ConnectorActions: Connector { payment_info: Option, ) -> Result> { let integration = self.get_data().connector.get_connector_integration(); - let request = self.generate_data( + let mut request = self.generate_data( types::PaymentsAuthorizeData { confirm: true, capture_method: Some(storage_models::enums::CaptureMethod::Automatic), @@ -65,6 +71,10 @@ pub trait ConnectorActions: Connector { }, payment_info, ); + let state = + routes::AppState::with_storage(Settings::new().unwrap(), StorageImpl::PostgresqlTest) + .await; + integration.execute_pretasks(&mut request, &state).await?; call_connector(request, integration).await } @@ -342,8 +352,8 @@ pub trait ConnectorActions: Connector { &self, req: Req, info: Option, - ) -> types::RouterData { - types::RouterData { + ) -> RouterData { + RouterData { flow: PhantomData, merchant_id: self.get_name(), connector: self.get_name(), @@ -369,9 +379,11 @@ pub trait ConnectorActions: Connector { .and_then(|a| a.address) .or_else(|| Some(PaymentAddress::default())) .unwrap(), - connector_meta_data: self.get_connector_meta(), + connector_meta_data: info.clone().and_then(|a| a.connector_meta_data), amount_captured: None, access_token: info.and_then(|a| a.access_token), + session_token: None, + reference_id: None, } } @@ -384,6 +396,7 @@ pub trait ConnectorActions: Connector { resource_id.get_connector_transaction_id().ok() } Ok(types::PaymentsResponseData::SessionResponse { .. }) => None, + Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None, Err(_) => None, } } @@ -394,10 +407,9 @@ async fn call_connector< Req: Debug + Clone + 'static, Resp: Debug + Clone + 'static, >( - request: types::RouterData, + request: RouterData, integration: services::BoxedConnectorIntegration<'_, T, Req, Resp>, -) -> Result, Report> { - use router::configs::settings::Settings; +) -> Result, Report> { let conf = Settings::new().unwrap(); let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest).await; services::api::execute_connector_processing_step( @@ -492,6 +504,7 @@ impl Default for PaymentCancelType { Self(types::PaymentsCancelData { cancellation_reason: Some("requested_by_customer".to_string()), connector_transaction_id: "".to_string(), + ..Default::default() }) } } @@ -551,6 +564,7 @@ pub fn get_connector_transaction_id( resource_id.get_connector_transaction_id().ok() } Ok(types::PaymentsResponseData::SessionResponse { .. }) => None, + Ok(types::PaymentsResponseData::SessionTokenResponse { .. }) => None, Err(_) => None, } } diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 7452ecda85d..6768003f210 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -296,7 +296,7 @@ async fn payments_create_core() { email: None, name: None, description: Some("Its my first payment request".to_string()), - return_url: Some("http://example.com/payments".to_string()), + return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: Some(api_enums::FutureUsage::OnSession), authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), payment_method_data: Some(api::PaymentMethod::Card(api::Card { @@ -451,7 +451,7 @@ async fn payments_create_core_adyen_no_redirect() { confirm: Some(true), customer_id: Some(customer_id), description: Some("Its my first payment request".to_string()), - return_url: Some("http://example.com/payments".to_string()), + return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: Some(api_enums::FutureUsage::OnSession), authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), payment_method_data: Some(api::PaymentMethod::Card(api::Card { diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index d7c80f560af..fea5b65d796 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -56,7 +56,7 @@ async fn payments_create_core() { email: None, name: None, description: Some("Its my first payment request".to_string()), - return_url: Some("http://example.com/payments".to_string()), + return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: None, authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), payment_method_data: Some(api::PaymentMethod::Card(api::Card { @@ -206,7 +206,7 @@ async fn payments_create_core_adyen_no_redirect() { confirm: Some(true), customer_id: Some(customer_id), description: Some("Its my first payment request".to_string()), - return_url: Some("http://example.com/payments".to_string()), + return_url: Some(url::Url::parse("http://example.com/payments").unwrap()), setup_future_usage: Some(api_enums::FutureUsage::OffSession), authentication_type: Some(api_enums::AuthenticationType::NoThreeDs), payment_method_data: Some(api::PaymentMethod::Card(api::Card { diff --git a/crates/storage_models/src/enums.rs b/crates/storage_models/src/enums.rs index 89e37f74996..1c9328bba98 100644 --- a/crates/storage_models/src/enums.rs +++ b/crates/storage_models/src/enums.rs @@ -25,6 +25,7 @@ pub mod diesel_exports { serde::Serialize, strum::Display, strum::EnumString, + frunk::LabelledGeneric, )] #[router_derive::diesel_enum(storage_type = "pg_enum")] #[serde(rename_all = "snake_case")] diff --git a/loadtest/config/Development.toml b/loadtest/config/Development.toml index 2887ec39221..9de76944e07 100644 --- a/loadtest/config/Development.toml +++ b/loadtest/config/Development.toml @@ -85,9 +85,36 @@ base_url = "https://apple-pay-gateway.apple.com/" [connectors.klarna] base_url = "https://api-na.playground.klarna.com/" +[connectors.bluesnap] +base_url = "https://sandbox.bluesnap.com/" + +[connectors.nuvei] +base_url = "https://ppp-test.nuvei.com/" + +[connectors.airwallex] +base_url = "https://api-demo.airwallex.com/" + [connectors.dlocal] base_url = "https://sandbox.dlocal.com/" [connectors.supported] wallets = ["klarna", "braintree", "applepay"] -cards = ["stripe", "adyen", "authorizedotnet", "checkout", "braintree", "cybersource", "shift4", "worldpay", "globalpay", "dlocal"] +cards = [ + "aci", + "adyen", + "airwallex", + "authorizedotnet", + "bluesnap", + "braintree", + "checkout", + "cybersource", + "dlocal", + "fiserv", + "globalpay", + "nuvei", + "payu", + "shift4", + "stripe", + "worldline", + "worldpay", +] diff --git a/openapi/generated.json b/openapi/generated.json index e94630c1e4c..0d3fc6023a3 100644 --- a/openapi/generated.json +++ b/openapi/generated.json @@ -1916,11 +1916,11 @@ "properties": { "label": { "type": "string", - "description": "the label must be non-empty to pass validation." + "description": "The label must be the name of the merchant." }, "type": { "type": "string", - "description": "The type of label" + "description": "A value that indicates whether the line item(Ex: total, tax, discount, or grand total) is final or pending." }, "amount": { "type": "string", @@ -1942,7 +1942,7 @@ } ] }, - "ApplePayRequest": { + "ApplePayPaymentRequest": { "type": "object", "required": [ "country_code", @@ -1979,7 +1979,7 @@ } } }, - "ApplePaySessionObject": { + "ApplePaySessionResponse": { "type": "object", "required": [ "epoch_timestamp", @@ -2011,7 +2011,7 @@ }, "nonce": { "type": "string", - "description": "Applepay generates unique ID (UUID) value" + "description": "Apple pay generated unique ID (UUID) value" }, "merchant_identifier": { "type": "string", @@ -2044,18 +2044,18 @@ } } }, - "ApplepayData": { + "ApplepaySessionTokenResponse": { "type": "object", "required": [ - "session_object", - "payment_request_object" + "session_token_data", + "payment_request_data" ], "properties": { - "session_object": { - "$ref": "#/components/schemas/ApplePaySessionObject" + "session_token_data": { + "$ref": "#/components/schemas/ApplePaySessionResponse" }, - "payment_request_object": { - "$ref": "#/components/schemas/ApplePayRequest" + "payment_request_data": { + "$ref": "#/components/schemas/ApplePayPaymentRequest" } } }, @@ -2843,7 +2843,19 @@ } } }, - "GpayData": { + "GpayMerchantInfo": { + "type": "object", + "required": [ + "merchant_name" + ], + "properties": { + "merchant_name": { + "type": "string", + "description": "The name of the merchant" + } + } + }, + "GpaySessionTokenResponse": { "type": "object", "required": [ "merchant_info", @@ -2865,18 +2877,6 @@ } } }, - "GpayMerchantInfo": { - "type": "object", - "required": [ - "merchant_name" - ], - "properties": { - "merchant_name": { - "type": "string", - "description": "The name of the merchant" - } - } - }, "GpayTokenParameters": { "type": "object", "required": [ @@ -2951,7 +2951,13 @@ "requires_capture" ] }, - "KlarnaData": { + "KlarnaIssuer": { + "type": "string", + "enum": [ + "klarna" + ] + }, + "KlarnaSessionTokenResponse": { "type": "object", "required": [ "session_token", @@ -2968,12 +2974,6 @@ } } }, - "KlarnaIssuer": { - "type": "string", - "enum": [ - "klarna" - ] - }, "ListCustomerPaymentMethodsResponse": { "type": "object", "required": [ @@ -3057,26 +3057,10 @@ ] }, "accepted_countries": { - "type": "array", - "items": { - "type": "string", - "description": "List of Countries accepted or has the processing capabilities of the processor" - }, - "example": [ - "US", - "UK", - "IN" - ] + "$ref": "#/components/schemas/admin.AcceptedCountries" }, "accepted_currencies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" - }, - "example": [ - "USD", - "EUR" - ] + "$ref": "#/components/schemas/admin.AcceptedCurrencies" }, "minimum_amount": { "type": "integer", @@ -3108,6 +3092,17 @@ "example": [ "redirect_to_url" ] + }, + "eligible_connectors": { + "type": "array", + "items": { + "type": "string", + "description": "Eligible connectors for this payment method" + }, + "example": [ + "stripe", + "adyen" + ] } } }, @@ -3659,14 +3654,31 @@ }, "example": [ { - "accepted_countries": [ - "in", - "us" - ], - "accepted_currencies": [ - "AED", - "AED" - ], + "accepted_countries": { + "disable_only": [ + "FR", + "DE", + "IN" + ], + "enable_all": false, + "enable_only": [ + "UK", + "AU" + ] + }, + "accepted_currencies": { + "disable_only": [ + "INR", + "CAD", + "AED", + "JPY" + ], + "enable_all": false, + "enable_only": [ + "EUR", + "USD" + ] + }, "installment_payment_enabled": true, "maximum_amount": 68607706, "minimum_amount": 1, @@ -4036,26 +4048,10 @@ ] }, "accepted_currencies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" - }, - "example": [ - "USD", - "EUR", - "AED" - ] + "$ref": "#/components/schemas/AcceptedCurrencies" }, "accepted_countries": { - "type": "array", - "items": { - "type": "string", - "description": "List of Countries accepted or has the processing capabilities of the processor" - }, - "example": [ - "US", - "IN" - ] + "$ref": "#/components/schemas/AcceptedCountries" }, "minimum_amount": { "type": "integer", @@ -4587,7 +4583,7 @@ } } }, - "PaypalData": { + "PaypalSessionTokenResponse": { "type": "object", "required": [ "session_token" @@ -4891,7 +4887,7 @@ { "allOf": [ { - "$ref": "#/components/schemas/GpayData" + "$ref": "#/components/schemas/GpaySessionTokenResponse" }, { "type": "object", @@ -4912,7 +4908,7 @@ { "allOf": [ { - "$ref": "#/components/schemas/KlarnaData" + "$ref": "#/components/schemas/KlarnaSessionTokenResponse" }, { "type": "object", @@ -4933,7 +4929,7 @@ { "allOf": [ { - "$ref": "#/components/schemas/PaypalData" + "$ref": "#/components/schemas/PaypalSessionTokenResponse" }, { "type": "object", @@ -4954,7 +4950,7 @@ { "allOf": [ { - "$ref": "#/components/schemas/ApplepayData" + "$ref": "#/components/schemas/ApplepaySessionTokenResponse" }, { "type": "object", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index e387e192c54..be5f201656e 100644 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -18,13 +18,13 @@ sed -i'' -e "s/};/${pg}::${pgc},\n};/" $conn.rs sed -i'' -e "s/_ => Err/\"${pg}\" => Ok(Box::new(\&connector::${pgc})),\n\t\t\t_ => Err/" $src/types/api.rs sed -i'' -e "s/pub supported: SupportedConnectors,/pub supported: SupportedConnectors,\n\tpub ${pg}: ConnectorParams,/" $src/configs/settings.rs sed -i'' -e "s/\[scheduler\]/[connectors.${pg}]\nbase_url = \"\"\n\n[scheduler]/" config/Development.toml -sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" config/Development.toml +sed -r -i'' -e "s/cards = \[/cards = [\n\t\"${pg}\",/" config/Development.toml sed -i'' -e "s/\[connectors.supported\]/[connectors.${pg}]\nbase_url = ""\n\n[connectors.supported]/" config/docker_compose.toml -sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" config/docker_compose.toml +sed -r -i'' -e "s/cards = \[/cards = [\n\t\"${pg}\",/" config/docker_compose.toml sed -i'' -e "s/\[connectors.supported\]/[connectors.${pg}]\nbase_url = ""\n\n[connectors.supported]/" config/config.example.toml -sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" config/config.example.toml +sed -r -i'' -e "s/cards = \[/cards = [\n\t\"${pg}\",/" config/config.example.toml sed -i'' -e "s/\[connectors.supported\]/[connectors.${pg}]\nbase_url = ""\n\n[connectors.supported]/" loadtest/config/Development.toml -sed -r -i'' -e "s/cards = \[(.*)\]/cards = [\1, \"${pg}\"]/" loadtest/config/Development.toml +sed -r -i'' -e "s/cards = \[/cards = [\n\t\"${pg}\",/" loadtest/config/Development.toml sed -i'' -e "s/Dummy,/Dummy,\n\t${pgc},/" crates/api_models/src/enums.rs sed -i'' -e "s/pub enum RoutableConnectors {/pub enum RoutableConnectors {\n\t${pgc},/" crates/api_models/src/enums.rs # remove temporary files created in above step