From 75ec96b6131d470b39171415058106b3464de75a Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:26:49 +0530 Subject: [PATCH 01/14] fix(connector): [Volt] handle 5xx error for Volt payments webhooks (#6576) --- .../hyperswitch_connectors/src/connectors/volt/transformers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs b/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs index b8e1e99054a..61cf91c87cb 100644 --- a/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs @@ -518,6 +518,7 @@ pub enum VoltWebhookObjectResource { #[derive(Debug, Deserialize, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct VoltPaymentWebhookObjectResource { + #[serde(alias = "id")] pub payment: String, pub merchant_internal_reference: Option, pub status: VoltWebhookPaymentStatus, From c04f81e3c4362369a92b2ead5ee1b28b4ca44b52 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:18:55 +0530 Subject: [PATCH 02/14] feat(users): Convert emails to lowercase from requests (#6601) --- crates/router/src/types/domain/user.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 5a881728b07..4cb69e68ed8 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -103,7 +103,7 @@ impl UserEmail { pub fn new(email: Secret) -> UserResult { use validator::ValidateEmail; - let email_string = email.expose(); + let email_string = email.expose().to_lowercase(); let email = pii::Email::from_str(&email_string).change_context(UserErrors::EmailParsingError)?; @@ -123,21 +123,8 @@ impl UserEmail { } pub fn from_pii_email(email: pii::Email) -> UserResult { - use validator::ValidateEmail; - - let email_string = email.peek(); - if email_string.validate_email() { - let (_username, domain) = match email_string.split_once('@') { - Some((u, d)) => (u, d), - None => return Err(UserErrors::EmailParsingError.into()), - }; - if BLOCKED_EMAIL.contains(domain) { - return Err(UserErrors::InvalidEmailError.into()); - } - Ok(Self(email)) - } else { - Err(UserErrors::EmailParsingError.into()) - } + let email_string = email.expose().map(|inner| inner.to_lowercase()); + Self::new(email_string) } pub fn into_inner(self) -> pii::Email { From 012e352db0477f5ddb4429cb0e4f5d781fd901a7 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:43:12 +0530 Subject: [PATCH 03/14] feat(router): Add support for network token migration (#6300) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/payment.rs | 25 +- crates/api_models/src/payment_methods.rs | 122 +++++++- crates/diesel_models/src/payment_method.rs | 26 ++ .../router/src/core/payment_methods/cards.rs | 283 ++++++++++++++++-- .../src/core/payment_methods/migration.rs | 61 ++++ .../router/src/core/payments/tokenization.rs | 4 +- .../router/src/types/api/payment_methods.rs | 13 +- 7 files changed, 487 insertions(+), 47 deletions(-) diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index b1f15188dcb..6fdb7d59b0f 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -18,7 +18,7 @@ use crate::{ ListCountriesCurrenciesResponse, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCollectLinkResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, PaymentMethodListResponse, - PaymentMethodResponse, PaymentMethodUpdate, + PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ self, ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints, @@ -218,6 +218,29 @@ impl ApiEventMetric for PaymentMethodResponse { } } +impl ApiEventMetric for PaymentMethodMigrateResponse { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_response.payment_method_id.clone(), + payment_method: self.payment_method_response.payment_method, + payment_method_type: self.payment_method_response.payment_method_type, + }) + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_response.payment_method_id.clone(), + payment_method: self.payment_method_response.payment_method_type, + payment_method_type: self.payment_method_response.payment_method_subtype, + }) + } +} + impl ApiEventMetric for PaymentMethodUpdate {} impl ApiEventMetric for DefaultPaymentMethod { diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 5baf138b090..0343ee02119 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -245,6 +245,9 @@ pub struct PaymentMethodMigrate { /// Card Details pub card: Option, + /// Network token details + pub network_token: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. pub metadata: Option, @@ -276,6 +279,24 @@ pub struct PaymentMethodMigrate { pub network_transaction_id: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentMethodMigrateResponse { + //payment method response when payment method entry is created + pub payment_method_response: PaymentMethodResponse, + + //card data migration status + pub card_migrated: Option, + + //network token data migration status + pub network_token_migrated: Option, + + //connector mandate details migration status + pub connector_mandate_details_migrated: Option, + + //network transaction id migration status + pub network_transaction_id_migrated: Option, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentsMandateReference( pub HashMap, @@ -540,6 +561,53 @@ pub struct MigrateCardDetail { pub card_type: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct MigrateNetworkTokenData { + /// Network Token Number + #[schema(value_type = String,example = "4111111145551142")] + pub network_token_number: CardNumber, + + /// Network Token Expiry Month + #[schema(value_type = String,example = "10")] + pub network_token_exp_month: masking::Secret, + + /// Network Token Expiry Year + #[schema(value_type = String,example = "25")] + pub network_token_exp_year: masking::Secret, + + /// Card Holder Name + #[schema(value_type = String,example = "John Doe")] + pub card_holder_name: Option>, + + /// Card Holder's Nick Name + #[schema(value_type = Option,example = "John Doe")] + pub nick_name: Option>, + + /// Card Issuing Country + pub card_issuing_country: Option, + + /// Card's Network + #[schema(value_type = Option)] + pub card_network: Option, + + /// Issuer Bank for Card + pub card_issuer: Option, + + /// Card Type + pub card_type: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct MigrateNetworkTokenDetail { + /// Network token details + pub network_token_data: MigrateNetworkTokenData, + + /// Network token requestor reference id + pub network_token_requestor_ref_id: String, +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -2082,6 +2150,10 @@ pub struct PaymentMethodRecord { pub original_transaction_amount: Option, pub original_transaction_currency: Option, pub line_number: Option, + pub network_token_number: Option, + pub network_token_expiry_month: Option>, + pub network_token_expiry_year: Option>, + pub network_token_requestor_ref_id: Option, } #[derive(Debug, Default, serde::Serialize)] @@ -2098,6 +2170,10 @@ pub struct PaymentMethodMigrationResponse { #[serde(skip_serializing_if = "Option::is_none")] pub migration_error: Option, pub card_number_masked: Option>, + pub card_migrated: Option, + pub network_token_migrated: Option, + pub connector_mandate_details_migrated: Option, + pub network_transaction_id_migrated: Option, } #[derive(Debug, Default, serde::Serialize)] @@ -2107,8 +2183,10 @@ pub enum MigrationStatus { Failed, } -type PaymentMethodMigrationResponseType = - (Result, PaymentMethodRecord); +type PaymentMethodMigrationResponseType = ( + Result, + PaymentMethodRecord, +); #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") @@ -2117,14 +2195,18 @@ impl From for PaymentMethodMigrationResponse fn from((response, record): PaymentMethodMigrationResponseType) -> Self { match response { Ok(res) => Self { - payment_method_id: Some(res.payment_method_id), - payment_method: res.payment_method, - payment_method_type: res.payment_method_type, - customer_id: res.customer_id, + payment_method_id: Some(res.payment_method_response.payment_method_id), + payment_method: res.payment_method_response.payment_method, + payment_method_type: res.payment_method_response.payment_method_type, + customer_id: res.payment_method_response.customer_id, migration_status: MigrationStatus::Success, migration_error: None, card_number_masked: Some(record.card_number_masked), line_number: record.line_number, + card_migrated: res.card_migrated, + network_token_migrated: res.network_token_migrated, + connector_mandate_details_migrated: res.connector_mandate_details_migrated, + network_transaction_id_migrated: res.network_transaction_id_migrated, }, Err(e) => Self { customer_id: Some(record.customer_id), @@ -2143,14 +2225,18 @@ impl From for PaymentMethodMigrationResponse fn from((response, record): PaymentMethodMigrationResponseType) -> Self { match response { Ok(res) => Self { - payment_method_id: Some(res.payment_method_id), - payment_method: res.payment_method_type, - payment_method_type: res.payment_method_subtype, - customer_id: Some(res.customer_id), + payment_method_id: Some(res.payment_method_response.payment_method_id), + payment_method: res.payment_method_response.payment_method_type, + payment_method_type: res.payment_method_response.payment_method_subtype, + customer_id: Some(res.payment_method_response.customer_id), migration_status: MigrationStatus::Success, migration_error: None, card_number_masked: Some(record.card_number_masked), line_number: record.line_number, + card_migrated: res.card_migrated, + network_token_migrated: res.network_token_migrated, + connector_mandate_details_migrated: res.connector_mandate_details_migrated, + network_transaction_id_migrated: res.network_transaction_id_migrated, }, Err(e) => Self { customer_id: Some(record.customer_id), @@ -2213,6 +2299,22 @@ impl card_issuing_country: None, nick_name: Some(record.nick_name.clone()), }), + network_token: Some(MigrateNetworkTokenDetail { + network_token_data: MigrateNetworkTokenData { + network_token_number: record.network_token_number.unwrap_or_default(), + network_token_exp_month: record.network_token_expiry_month.unwrap_or_default(), + network_token_exp_year: record.network_token_expiry_year.unwrap_or_default(), + card_holder_name: record.name, + nick_name: Some(record.nick_name.clone()), + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, + }, + network_token_requestor_ref_id: record + .network_token_requestor_ref_id + .unwrap_or_default(), + }), payment_method: record.payment_method, payment_method_type: record.payment_method_type, payment_method_issuer: None, diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index d607cd04bff..55460037855 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -248,6 +248,11 @@ pub enum PaymentMethodUpdate { ConnectorMandateDetailsUpdate { connector_mandate_details: Option, }, + NetworkTokenDataUpdate { + network_token_requestor_reference_id: Option, + network_token_locker_id: Option, + network_token_payment_method_data: Option, + }, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -620,6 +625,27 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_payment_method_data: None, }, + PaymentMethodUpdate::NetworkTokenDataUpdate { + network_token_requestor_reference_id, + network_token_locker_id, + network_token_payment_method_data, + } => Self { + metadata: None, + payment_method_data: None, + last_used_at: None, + status: None, + locker_id: None, + payment_method: None, + connector_mandate_details: None, + updated_by: None, + payment_method_issuer: None, + payment_method_type: None, + last_modified: common_utils::date_time::now(), + network_transaction_id: None, + network_token_requestor_reference_id, + network_token_locker_id, + network_token_payment_method_data, + }, } } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 49a75735ea4..4073b7a4d9b 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -57,9 +57,12 @@ use masking::Secret; use router_env::{instrument, metrics::add_attributes, tracing}; use strum::IntoEnumIterator; -use super::surcharge_decision_configs::{ - perform_surcharge_decision_management_for_payment_method_list, - perform_surcharge_decision_management_for_saved_cards, +use super::{ + migration::RecordMigrationStatusBuilder, + surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, + }, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -376,7 +379,7 @@ pub async fn migrate_payment_method( merchant_id: &id_type::MerchantId, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, -) -> errors::RouterResponse { +) -> errors::RouterResponse { let mut req = req; let card_details = req.card.as_ref().get_required_value("card")?; @@ -405,34 +408,92 @@ pub async fn migrate_payment_method( .await?; }; - match card_number_validation_result { + let should_require_connector_mandate_details = req.network_token.is_none(); + + let mut migration_status = RecordMigrationStatusBuilder::new(); + + let resp = match card_number_validation_result { Ok(card_number) => { let payment_method_create_request = api::PaymentMethodCreate::get_payment_method_create_from_payment_method_migrate( card_number, &req, ); + + logger::debug!("Storing the card in locker and migrating the payment method"); get_client_secret_or_add_payment_method_for_migration( &state, payment_method_create_request, merchant_account, key_store, + &mut migration_status, ) - .await + .await? } Err(card_validation_error) => { logger::debug!("Card number to be migrated is invalid, skip saving in locker {card_validation_error}"); skip_locker_call_and_migrate_payment_method( - state, + &state, &req, merchant_id.to_owned(), key_store, merchant_account, card_bin_details.clone(), + should_require_connector_mandate_details, + &mut migration_status, ) - .await + .await? } - } + }; + let payment_method_response = match resp { + services::ApplicationResponse::Json(response) => response, + _ => Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch the payment method response")?, + }; + + let pm_id = payment_method_response.payment_method_id.clone(); + + let network_token = req.network_token.clone(); + + let network_token_migrated = match network_token { + Some(nt_detail) => { + logger::debug!("Network token migration"); + let network_token_requestor_ref_id = nt_detail.network_token_requestor_ref_id.clone(); + let network_token_data = &nt_detail.network_token_data; + + Some( + save_network_token_and_update_payment_method( + &state, + &req, + key_store, + merchant_account, + network_token_data, + network_token_requestor_ref_id, + pm_id, + ) + .await + .map_err(|err| logger::error!(?err, "Failed to save network token")) + .ok() + .unwrap_or_default(), + ) + } + None => { + logger::debug!("Network token data is not available"); + None + } + }; + migration_status.network_token_migrated(network_token_migrated); + let migrate_status = migration_status.build(); + + Ok(services::ApplicationResponse::Json( + api::PaymentMethodMigrateResponse { + payment_method_response, + card_migrated: migrate_status.card_migrated, + network_token_migrated: migrate_status.network_token_migrated, + connector_mandate_details_migrated: migrate_status.connector_mandate_details_migrated, + network_transaction_id_migrated: migrate_status.network_transaction_migrated, + }, + )) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -442,7 +503,7 @@ pub async fn migrate_payment_method( _merchant_id: &id_type::MerchantId, _merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, -) -> errors::RouterResponse { +) -> errors::RouterResponse { todo!() } @@ -645,31 +706,49 @@ impl not(feature = "payment_methods_v2"), not(feature = "customer_v2") ))] +#[allow(clippy::too_many_arguments)] pub async fn skip_locker_call_and_migrate_payment_method( - state: routes::SessionState, + state: &routes::SessionState, req: &api::PaymentMethodMigrate, merchant_id: id_type::MerchantId, key_store: &domain::MerchantKeyStore, merchant_account: &domain::MerchantAccount, card: api_models::payment_methods::CardDetailFromLocker, + should_require_connector_mandate_details: bool, + migration_status: &mut RecordMigrationStatusBuilder, ) -> errors::RouterResponse { let db = &*state.store; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; // In this case, since we do not have valid card details, recurring payments can only be done through connector mandate details. - let connector_mandate_details_req = req - .connector_mandate_details - .clone() - .get_required_value("connector mandate details")?; + //if network token data is present, then connector mandate details are not mandatory - let connector_mandate_details = serde_json::to_value(&connector_mandate_details_req) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to parse connector mandate details")?; - let key_manager_state = (&state).into(); + let connector_mandate_details = if should_require_connector_mandate_details { + let connector_mandate_details_req = req + .connector_mandate_details + .clone() + .get_required_value("connector mandate details")?; + + Some( + serde_json::to_value(&connector_mandate_details_req) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse connector mandate details")?, + ) + } else { + req.connector_mandate_details + .clone() + .map(|connector_mandate_details_req| { + serde_json::to_value(&connector_mandate_details_req) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse connector mandate details") + }) + .transpose()? + }; + let key_manager_state = &state.into(); let payment_method_billing_address: Option>> = req .billing .clone() - .async_map(|billing| create_encrypted_data(&key_manager_state, key_store, billing)) + .async_map(|billing| create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -677,7 +756,7 @@ pub async fn skip_locker_call_and_migrate_payment_method( let customer = db .find_customer_by_customer_id_merchant_id( - &(&state).into(), + &state.into(), &customer_id, &merchant_id, key_store, @@ -690,7 +769,7 @@ pub async fn skip_locker_call_and_migrate_payment_method( PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())); let payment_method_data_encrypted: Option>> = Some( - create_encrypted_data(&key_manager_state, key_store, payment_method_card_details) + create_encrypted_data(key_manager_state, key_store, payment_method_card_details) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt Payment method card details")?, @@ -699,13 +778,26 @@ pub async fn skip_locker_call_and_migrate_payment_method( let payment_method_metadata: Option = req.metadata.as_ref().map(|data| data.peek()).cloned(); + let network_transaction_id = req.network_transaction_id.clone(); + + migration_status.network_transaction_id_migrated(network_transaction_id.as_ref().map(|_| true)); + + migration_status.connector_mandate_details_migrated( + connector_mandate_details + .as_ref() + .map(|_| true) + .or_else(|| req.connector_mandate_details.as_ref().map(|_| false)), + ); + + migration_status.card_migrated(false); + let payment_method_id = generate_id(consts::ID_LENGTH, "pm"); let current_time = common_utils::date_time::now(); let response = db .insert_payment_method( - &(&state).into(), + &state.into(), key_store, domain::PaymentMethod { customer_id: customer_id.to_owned(), @@ -718,11 +810,11 @@ pub async fn skip_locker_call_and_migrate_payment_method( scheme: req.card_network.clone().or(card.scheme.clone()), metadata: payment_method_metadata.map(Secret::new), payment_method_data: payment_method_data_encrypted.map(Into::into), - connector_mandate_details: Some(connector_mandate_details), + connector_mandate_details, customer_acceptance: None, client_secret: None, status: enums::PaymentMethodStatus::Active, - network_transaction_id: None, + network_transaction_id, payment_method_issuer_code: None, accepted_currency: None, token: None, @@ -753,7 +845,7 @@ pub async fn skip_locker_call_and_migrate_payment_method( if customer.default_payment_method_id.is_none() && req.payment_method.is_some() { let _ = set_default_payment_method( - &state, + state, &merchant_id, key_store.clone(), &customer_id, @@ -768,6 +860,117 @@ pub async fn skip_locker_call_and_migrate_payment_method( )) } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2"), + not(feature = "customer_v2") +))] +#[allow(clippy::too_many_arguments)] +pub async fn save_network_token_and_update_payment_method( + state: &routes::SessionState, + req: &api::PaymentMethodMigrate, + key_store: &domain::MerchantKeyStore, + merchant_account: &domain::MerchantAccount, + network_token_data: &api_models::payment_methods::MigrateNetworkTokenData, + network_token_requestor_ref_id: String, + pm_id: String, +) -> errors::RouterResult { + let payment_method_create_request = + api::PaymentMethodCreate::get_payment_method_create_from_payment_method_migrate( + network_token_data.network_token_number.clone(), + req, + ); + let customer_id = req.customer_id.clone().get_required_value("customer_id")?; + + let network_token_details = api::CardDetail { + card_number: network_token_data.network_token_number.clone(), + card_exp_month: network_token_data.network_token_exp_month.clone(), + card_exp_year: network_token_data.network_token_exp_year.clone(), + card_holder_name: network_token_data.card_holder_name.clone(), + nick_name: network_token_data.nick_name.clone(), + card_issuing_country: network_token_data.card_issuing_country.clone(), + card_network: network_token_data.card_network.clone(), + card_issuer: network_token_data.card_issuer.clone(), + card_type: network_token_data.card_type.clone(), + }; + + logger::debug!( + "Adding network token to locker for customer_id: {:?}", + customer_id + ); + + let token_resp = Box::pin(add_card_to_locker( + state, + payment_method_create_request.clone(), + &network_token_details, + &customer_id, + merchant_account, + None, + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Network Token failed"); + let key_manager_state = &state.into(); + + match token_resp { + Ok(resp) => { + logger::debug!("Network token added to locker"); + let (token_pm_resp, _duplication_check) = resp; + let pm_token_details = token_pm_resp + .card + .as_ref() + .map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone()))); + let pm_network_token_data_encrypted = pm_token_details + .async_map(|pm_card| create_encrypted_data(key_manager_state, key_store, pm_card)) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt payment method data")?; + + let pm_update = storage::PaymentMethodUpdate::NetworkTokenDataUpdate { + network_token_requestor_reference_id: Some(network_token_requestor_ref_id), + network_token_locker_id: Some(token_pm_resp.payment_method_id), + network_token_payment_method_data: pm_network_token_data_encrypted.map(Into::into), + }; + let db = &*state.store; + let existing_pm = db + .find_payment_method( + &state.into(), + key_store, + &pm_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Failed to fetch payment method for existing pm_id: {:?} in db", + pm_id + ))?; + + db.update_payment_method( + &state.into(), + key_store, + existing_pm, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Failed to update payment method for existing pm_id: {:?} in db", + pm_id + ))?; + + logger::debug!("Network token added to locker and payment method updated"); + Ok(true) + } + Err(err) => { + logger::debug!("Network token added to locker failed {:?}", err); + Ok(false) + } + } +} + // need to discuss regarding the migration APIs for v2 #[cfg(all( feature = "v2", @@ -899,6 +1102,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration( req: api::PaymentMethodCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, + migration_status: &mut RecordMigrationStatusBuilder, ) -> errors::RouterResponse { let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.clone().get_required_value("customer_id")?; @@ -908,6 +1112,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration( #[cfg(feature = "payouts")] let condition = req.card.is_some() || req.bank_transfer.is_some() || req.wallet.is_some(); let key_manager_state = state.into(); + let payment_method_billing_address: Option>> = req .billing .clone() @@ -930,6 +1135,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration( req, merchant_account, key_store, + migration_status, )) .await } else { @@ -946,7 +1152,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration( None, None, key_store, - connector_mandate_details, + connector_mandate_details.clone(), Some(enums::PaymentMethodStatus::AwaitingData), None, merchant_account.storage_scheme, @@ -957,6 +1163,14 @@ pub async fn get_client_secret_or_add_payment_method_for_migration( None, ) .await?; + migration_status.connector_mandate_details_migrated( + connector_mandate_details + .clone() + .map(|_| true) + .or_else(|| req.connector_mandate_details.clone().map(|_| false)), + ); + + migration_status.card_migrated(false); if res.status == enums::PaymentMethodStatus::AwaitingData { add_payment_method_status_update_task( @@ -1482,6 +1696,7 @@ pub async fn save_migration_payment_method( req: api::PaymentMethodCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, + migration_status: &mut RecordMigrationStatusBuilder, ) -> errors::RouterResponse { req.validate()?; let db = &*state.store; @@ -1505,6 +1720,17 @@ pub async fn save_migration_payment_method( .transpose() .change_context(errors::ApiErrorResponse::InternalServerError)?; + let network_transaction_id = req.network_transaction_id.clone(); + + migration_status.network_transaction_id_migrated(network_transaction_id.as_ref().map(|_| true)); + + migration_status.connector_mandate_details_migrated( + connector_mandate_details + .as_ref() + .map(|_| true) + .or_else(|| req.connector_mandate_details.as_ref().map(|_| false)), + ); + let response = match payment_method { #[cfg(feature = "payouts")] api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() { @@ -1564,6 +1790,7 @@ pub async fn save_migration_payment_method( let (mut resp, duplication_check) = response?; + migration_status.card_migrated(true); match duplication_check { Some(duplication_check) => match duplication_check { payment_methods::DataDuplicationCheck::Duplicated => { @@ -1714,7 +1941,7 @@ pub async fn save_migration_payment_method( None, locker_id, connector_mandate_details, - req.network_transaction_id.clone(), + network_transaction_id, merchant_account.storage_scheme, payment_method_billing_address.map(Into::into), None, diff --git a/crates/router/src/core/payment_methods/migration.rs b/crates/router/src/core/payment_methods/migration.rs index b0aaa767a4e..11bc3a9d74f 100644 --- a/crates/router/src/core/payment_methods/migration.rs +++ b/crates/router/src/core/payment_methods/migration.rs @@ -149,3 +149,64 @@ fn validate_card_exp_year(year: String) -> Result<(), errors::ValidationError> { }) } } + +#[derive(Debug)] +pub struct RecordMigrationStatus { + pub card_migrated: Option, + pub network_token_migrated: Option, + pub connector_mandate_details_migrated: Option, + pub network_transaction_migrated: Option, +} + +#[derive(Debug)] +pub struct RecordMigrationStatusBuilder { + pub card_migrated: Option, + pub network_token_migrated: Option, + pub connector_mandate_details_migrated: Option, + pub network_transaction_migrated: Option, +} + +impl RecordMigrationStatusBuilder { + pub fn new() -> Self { + Self { + card_migrated: None, + network_token_migrated: None, + connector_mandate_details_migrated: None, + network_transaction_migrated: None, + } + } + + pub fn card_migrated(&mut self, card_migrated: bool) { + self.card_migrated = Some(card_migrated); + } + + pub fn network_token_migrated(&mut self, network_token_migrated: Option) { + self.network_token_migrated = network_token_migrated; + } + + pub fn connector_mandate_details_migrated( + &mut self, + connector_mandate_details_migrated: Option, + ) { + self.connector_mandate_details_migrated = connector_mandate_details_migrated; + } + + pub fn network_transaction_id_migrated(&mut self, network_transaction_migrated: Option) { + self.network_transaction_migrated = network_transaction_migrated; + } + + pub fn build(self) -> RecordMigrationStatus { + RecordMigrationStatus { + card_migrated: self.card_migrated, + network_token_migrated: self.network_token_migrated, + connector_mandate_details_migrated: self.connector_mandate_details_migrated, + network_transaction_migrated: self.network_transaction_migrated, + } + } +} + +impl Default for RecordMigrationStatusBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 4bae04e1426..a6f3018ed0d 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -973,7 +973,7 @@ pub async fn save_network_token_in_locker( { Ok((token_response, network_token_requestor_ref_id)) => { // Only proceed if the tokenization was successful - let card_data = api::CardDetail { + let network_token_data = api::CardDetail { card_number: token_response.token.clone(), card_exp_month: token_response.token_expiry_month.clone(), card_exp_year: token_response.token_expiry_year.clone(), @@ -988,7 +988,7 @@ pub async fn save_network_token_in_locker( let (res, dc) = Box::pin(payment_methods::cards::add_card_to_locker( state, payment_method_request, - &card_data, + &network_token_data, &customer_id, merchant_account, None, diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 8b1175d5cc6..25227ae5383 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -7,9 +7,10 @@ pub use api_models::payment_methods::{ PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm, PaymentMethodIntentConfirmInternal, PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse, - PaymentMethodMigrate, PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, - PaymentMethodUpdateData, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, - TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, + PaymentMethodMigrate, PaymentMethodMigrateResponse, PaymentMethodResponse, + PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, + TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, + TokenizedWalletValue1, TokenizedWalletValue2, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -22,9 +23,9 @@ pub use api_models::payment_methods::{ PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, - PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, - TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, - TokenizedWalletValue2, + PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, + TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, + TokenizedWalletValue1, TokenizedWalletValue2, }; use error_stack::report; From 98aa84b7e842ac85ce2461f3eab826a6c3783832 Mon Sep 17 00:00:00 2001 From: Sagnik Mitra <83326850+ImSagnik007@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:55:08 +0530 Subject: [PATCH 04/14] fix(dispute): change dispute currency type to currency enum (#6454) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 3 +- api-reference/openapi_spec.json | 3 +- crates/api_models/src/disputes.rs | 11 +++--- crates/diesel_models/src/dispute.rs | 2 ++ crates/diesel_models/src/schema.rs | 1 + crates/diesel_models/src/schema_v2.rs | 1 + .../src/connectors/airwallex/transformers.rs | 2 +- .../src/connectors/novalnet.rs | 2 +- crates/hyperswitch_interfaces/src/disputes.rs | 2 +- .../src/compatibility/stripe/webhooks.rs | 4 +-- crates/router/src/connector/adyen.rs | 2 +- crates/router/src/connector/bluesnap.rs | 2 +- crates/router/src/connector/braintree.rs | 35 ++++++++----------- .../src/connector/braintree/transformers.rs | 2 +- .../src/connector/checkout/transformers.rs | 2 +- crates/router/src/connector/payme.rs | 2 +- crates/router/src/connector/paypal.rs | 2 +- crates/router/src/connector/rapyd.rs | 2 +- .../src/connector/stripe/transformers.rs | 3 +- .../src/connector/trustpay/transformers.rs | 2 +- crates/router/src/connector/utils.rs | 13 ++++++- crates/router/src/core/webhooks/incoming.rs | 3 +- crates/router/src/db/dispute.rs | 11 ++++-- crates/router/src/services/kafka/dispute.rs | 12 +++++-- .../src/services/kafka/dispute_event.rs | 11 ++++-- crates/router/src/types/storage/dispute.rs | 8 +---- crates/router/src/types/transformers.rs | 8 ++++- crates/router/src/utils/user/sample_data.rs | 1 + .../down.sql | 2 ++ .../up.sql | 2 ++ 30 files changed, 95 insertions(+), 61 deletions(-) create mode 100644 migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/down.sql create mode 100644 migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/up.sql diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 53b8c636dab..c6a1fad0d35 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -7192,8 +7192,7 @@ "description": "The dispute amount" }, "currency": { - "type": "string", - "description": "The three-letter ISO currency code" + "$ref": "#/components/schemas/Currency" }, "dispute_stage": { "$ref": "#/components/schemas/DisputeStage" diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 1ba6ac636b8..353e9d5d49b 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -10040,8 +10040,7 @@ "description": "The dispute amount" }, "currency": { - "type": "string", - "description": "The three-letter ISO currency code" + "$ref": "#/components/schemas/Currency" }, "dispute_stage": { "$ref": "#/components/schemas/DisputeStage" diff --git a/crates/api_models/src/disputes.rs b/crates/api_models/src/disputes.rs index 4ddb50af0f2..5b0de11f2d0 100644 --- a/crates/api_models/src/disputes.rs +++ b/crates/api_models/src/disputes.rs @@ -6,8 +6,8 @@ use serde::de::Error; use time::PrimitiveDateTime; use utoipa::ToSchema; -use super::enums::{DisputeStage, DisputeStatus}; -use crate::{admin::MerchantConnectorInfo, enums, files}; +use super::enums::{Currency, DisputeStage, DisputeStatus}; +use crate::{admin::MerchantConnectorInfo, files}; #[derive(Clone, Debug, Serialize, ToSchema, Eq, PartialEq)] pub struct DisputeResponse { @@ -21,7 +21,8 @@ pub struct DisputeResponse { /// The dispute amount pub amount: String, /// The three-letter ISO currency code - pub currency: String, + #[schema(value_type = Currency)] + pub currency: Currency, /// Stage of the dispute pub dispute_stage: DisputeStage, /// Status of the dispute @@ -137,7 +138,7 @@ pub struct DisputeListGetConstraints { pub connector: Option>, /// The comma separated list of currencies of the disputes #[serde(default, deserialize_with = "parse_comma_separated")] - pub currency: Option>, + pub currency: Option>, /// The merchant connector id to filter the disputes list pub merchant_connector_id: Option, /// The time range for which objects are needed. TimeRange has two fields start_time and end_time from which objects can be filtered as per required scenarios (created_at, time less than, greater than etc). @@ -150,7 +151,7 @@ pub struct DisputeListFilters { /// The map of available connector filters, where the key is the connector name and the value is a list of MerchantConnectorInfo instances pub connector: HashMap>, /// The list of available currency filters - pub currency: Vec, + pub currency: Vec, /// The list of available dispute status filters pub dispute_status: Vec, /// The list of available dispute stage filters diff --git a/crates/diesel_models/src/dispute.rs b/crates/diesel_models/src/dispute.rs index 130e46aa9cc..8e3bab20d89 100644 --- a/crates/diesel_models/src/dispute.rs +++ b/crates/diesel_models/src/dispute.rs @@ -31,6 +31,7 @@ pub struct DisputeNew { pub merchant_connector_id: Option, pub dispute_amount: i64, pub organization_id: common_utils::id_type::OrganizationId, + pub dispute_currency: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Identifiable, Queryable, Selectable)] @@ -61,6 +62,7 @@ pub struct Dispute { pub merchant_connector_id: Option, pub dispute_amount: i64, pub organization_id: common_utils::id_type::OrganizationId, + pub dispute_currency: Option, } #[derive(Debug)] diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 19a0763d770..2b815db390c 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -384,6 +384,7 @@ diesel::table! { dispute_amount -> Int8, #[max_length = 32] organization_id -> Varchar, + dispute_currency -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index aa7f3f7232c..aea22e3d7c6 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -396,6 +396,7 @@ diesel::table! { dispute_amount -> Int8, #[max_length = 32] organization_id -> Varchar, + dispute_currency -> Nullable, } } diff --git a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs index bb3bc399077..040a1422e0e 100644 --- a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs @@ -829,7 +829,7 @@ pub struct AirwallexObjectData { pub struct AirwallexDisputeObject { pub payment_intent_id: String, pub dispute_amount: i64, - pub dispute_currency: String, + pub dispute_currency: enums::Currency, pub stage: AirwallexDisputeStage, pub dispute_id: String, pub dispute_reason_type: Option, diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet.rs b/crates/hyperswitch_connectors/src/connectors/novalnet.rs index 8331e229acb..fd7ff906d08 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet.rs @@ -906,7 +906,7 @@ impl webhooks::IncomingWebhook for Novalnet { novalnet::get_novalnet_dispute_status(notif.event.event_type).to_string(); Ok(disputes::DisputePayload { amount: novalnet::option_to_result(amount)?.to_string(), - currency: novalnet::option_to_result(currency)?.to_string(), + currency: novalnet::option_to_result(currency)?, dispute_stage: api_models::enums::DisputeStage::Dispute, connector_dispute_id: notif.event.tid.to_string(), connector_reason: reason, diff --git a/crates/hyperswitch_interfaces/src/disputes.rs b/crates/hyperswitch_interfaces/src/disputes.rs index acc7b56e906..55a6d13c91d 100644 --- a/crates/hyperswitch_interfaces/src/disputes.rs +++ b/crates/hyperswitch_interfaces/src/disputes.rs @@ -8,7 +8,7 @@ pub struct DisputePayload { /// amount pub amount: String, /// currency - pub currency: String, + pub currency: common_enums::enums::Currency, /// dispute_stage pub dispute_stage: common_enums::enums::DisputeStage, /// connector_status diff --git a/crates/router/src/compatibility/stripe/webhooks.rs b/crates/router/src/compatibility/stripe/webhooks.rs index ddc291e6b13..2212a8953fc 100644 --- a/crates/router/src/compatibility/stripe/webhooks.rs +++ b/crates/router/src/compatibility/stripe/webhooks.rs @@ -1,7 +1,7 @@ #[cfg(feature = "payouts")] use api_models::payouts as payout_models; use api_models::{ - enums::{DisputeStatus, MandateStatus}, + enums::{Currency, DisputeStatus, MandateStatus}, webhooks::{self as api}, }; #[cfg(feature = "payouts")] @@ -93,7 +93,7 @@ pub enum StripeWebhookObject { pub struct StripeDisputeResponse { pub id: String, pub amount: String, - pub currency: String, + pub currency: Currency, pub payment_intent: common_utils::id_type::PaymentId, pub reason: Option, pub status: StripeDisputeStatus, diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 7b491674e92..c9ccd8a9c62 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -1895,7 +1895,7 @@ impl api::IncomingWebhook for Adyen { .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; Ok(api::disputes::DisputePayload { amount: notif.amount.value.to_string(), - currency: notif.amount.currency.to_string(), + currency: notif.amount.currency, dispute_stage: api_models::enums::DisputeStage::from(notif.event_code.clone()), connector_dispute_id: notif.psp_reference, connector_reason: notif.reason, diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 468f68dc340..181f1dbf3d7 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -1149,7 +1149,7 @@ impl api::IncomingWebhook for Bluesnap { dispute_details.invoice_charge_amount.abs().to_string(), dispute_details.currency, )?, - currency: dispute_details.currency.to_string(), + currency: dispute_details.currency, dispute_stage: api_models::enums::DisputeStage::Dispute, connector_dispute_id: dispute_details.reversal_ref_num, connector_reason: dispute_details.reversal_reason, diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 85b310397b2..dba26121880 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -1,5 +1,4 @@ pub mod transformers; -use std::str::FromStr; use api_models::webhooks::IncomingWebhookEvent; use base64::Engine; @@ -998,25 +997,21 @@ impl api::IncomingWebhook for Braintree { let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; match response.dispute { - Some(dispute_data) => { - let currency = enums::Currency::from_str(dispute_data.currency_iso_code.as_str()) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Ok(api::disputes::DisputePayload { - amount: connector_utils::to_currency_lower_unit( - dispute_data.amount_disputed.to_string(), - currency, - )?, - currency: dispute_data.currency_iso_code, - dispute_stage: transformers::get_dispute_stage(dispute_data.kind.as_str())?, - connector_dispute_id: dispute_data.id, - connector_reason: dispute_data.reason, - connector_reason_code: dispute_data.reason_code, - challenge_required_by: dispute_data.reply_by_date, - connector_status: dispute_data.status, - created_at: dispute_data.created_at, - updated_at: dispute_data.updated_at, - }) - } + Some(dispute_data) => Ok(api::disputes::DisputePayload { + amount: connector_utils::to_currency_lower_unit( + dispute_data.amount_disputed.to_string(), + dispute_data.currency_iso_code, + )?, + currency: dispute_data.currency_iso_code, + dispute_stage: transformers::get_dispute_stage(dispute_data.kind.as_str())?, + connector_dispute_id: dispute_data.id, + connector_reason: dispute_data.reason, + connector_reason_code: dispute_data.reason_code, + challenge_required_by: dispute_data.reply_by_date, + connector_status: dispute_data.status, + created_at: dispute_data.created_at, + updated_at: dispute_data.updated_at, + }), None => Err(errors::ConnectorError::WebhookResourceObjectNotFound)?, } } diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index bcef8124c65..24c6118e44a 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -1766,7 +1766,7 @@ pub struct BraintreeDisputeData { pub amount_won: Option, pub case_number: Option, pub chargeback_protection_level: Option, - pub currency_iso_code: String, + pub currency_iso_code: enums::Currency, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created_at: Option, pub evidence: Option, diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 1f6b737367d..0d4d8201003 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -1252,7 +1252,7 @@ pub struct CheckoutDisputeWebhookData { pub payment_id: Option, pub action_id: Option, pub amount: i32, - pub currency: String, + pub currency: enums::Currency, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub evidence_required_by: Option, pub reason_code: Option, diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index 6bf26b4f60b..bc5524e5c8f 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -1280,7 +1280,7 @@ impl api::IncomingWebhook for Payme { Ok(api::disputes::DisputePayload { amount: webhook_object.price.to_string(), - currency: webhook_object.currency.to_string(), + currency: webhook_object.currency, dispute_stage: api_models::enums::DisputeStage::Dispute, connector_dispute_id: webhook_object.payme_transaction_id, connector_reason: None, diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 48a35c78cc2..e79912f61ff 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -2001,7 +2001,7 @@ impl api::IncomingWebhook for Paypal { payload.dispute_amount.value.get_amount_as_string(), payload.dispute_amount.currency_code, )?, - currency: payload.dispute_amount.currency_code.to_string(), + currency: payload.dispute_amount.currency_code, dispute_stage: api_models::enums::DisputeStage::from( payload.dispute_life_cycle_stage.clone(), ), diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index 196eb4ce73d..64c3fcdf5a1 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -974,7 +974,7 @@ impl api::IncomingWebhook for Rapyd { }?; Ok(api::disputes::DisputePayload { amount: webhook_dispute_data.amount.to_string(), - currency: webhook_dispute_data.currency.to_string(), + currency: webhook_dispute_data.currency, dispute_stage: api_models::enums::DisputeStage::Dispute, connector_dispute_id: webhook_dispute_data.token, connector_reason: Some(webhook_dispute_data.dispute_reason_description), diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 32fa8da0320..6d9a4f1c459 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -3640,7 +3640,8 @@ pub struct WebhookEventObjectData { pub id: String, pub object: WebhookEventObjectType, pub amount: Option, - pub currency: String, + #[serde(default, deserialize_with = "connector_util::convert_uppercase")] + pub currency: enums::Currency, pub payment_intent: Option, pub client_secret: Option>, pub reason: Option, diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 8312b3e2659..dafca256529 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -1790,7 +1790,7 @@ pub struct WebhookReferences { #[serde(rename_all = "PascalCase")] pub struct WebhookAmount { pub amount: f64, - pub currency: String, + pub currency: enums::Currency, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 687edd5aa23..ed4b97e2a99 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -28,7 +28,7 @@ use hyperswitch_domain_models::{ SyncIntegrityObject, }, }; -use masking::{ExposeInterface, Secret}; +use masking::{Deserialize, ExposeInterface, Secret}; use once_cell::sync::Lazy; use regex::Regex; use serde::Serializer; @@ -3143,3 +3143,14 @@ impl NetworkTokenData for domain::NetworkTokenData { Secret::new(year) } } + +pub fn convert_uppercase<'de, D, T>(v: D) -> Result +where + D: serde::Deserializer<'de>, + T: FromStr, + ::Err: std::fmt::Debug + std::fmt::Display + std::error::Error, +{ + use serde::de::Error; + let output = <&str>::deserialize(v)?; + output.to_uppercase().parse::().map_err(D::Error::custom) +} diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index b049ffdb129..3532f1e3fd7 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -942,7 +942,7 @@ async fn get_or_update_dispute_object( let new_dispute = diesel_models::dispute::DisputeNew { dispute_id, amount: dispute_details.amount.clone(), - currency: dispute_details.currency, + currency: dispute_details.currency.to_string(), dispute_stage: dispute_details.dispute_stage, dispute_status: common_enums::DisputeStatus::foreign_try_from(event_type) .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) @@ -963,6 +963,7 @@ async fn get_or_update_dispute_object( merchant_connector_id: payment_attempt.merchant_connector_id.clone(), dispute_amount: dispute_details.amount.parse::().unwrap_or(0), organization_id: organization_id.clone(), + dispute_currency: Some(dispute_details.currency), }; state .store diff --git a/crates/router/src/db/dispute.rs b/crates/router/src/db/dispute.rs index d7f4af63015..054f949059d 100644 --- a/crates/router/src/db/dispute.rs +++ b/crates/router/src/db/dispute.rs @@ -199,6 +199,7 @@ impl DisputeInterface for MockDb { merchant_connector_id: dispute.merchant_connector_id, dispute_amount: dispute.dispute_amount, organization_id: dispute.organization_id, + dispute_currency: dispute.dispute_currency, }; locked_disputes.push(new_dispute.clone()); @@ -308,9 +309,12 @@ impl DisputeInterface for MockDb { .currency .as_ref() .map_or(true, |currencies| { - currencies - .iter() - .any(|currency| dispute.currency.as_str() == currency.to_string()) + currencies.iter().any(|currency| { + dispute + .dispute_currency + .map(|dispute_currency| &dispute_currency == currency) + .unwrap_or(dispute.currency.as_str() == currency.to_string()) + }) }) && dispute_constraints .time_range @@ -502,6 +506,7 @@ mod tests { merchant_connector_id: None, dispute_amount: 1040, organization_id: common_utils::id_type::OrganizationId::default(), + dispute_currency: Some(common_enums::Currency::default()), } } diff --git a/crates/router/src/services/kafka/dispute.rs b/crates/router/src/services/kafka/dispute.rs index 2afb41cc867..cc3a538851e 100644 --- a/crates/router/src/services/kafka/dispute.rs +++ b/crates/router/src/services/kafka/dispute.rs @@ -1,4 +1,4 @@ -use common_utils::id_type; +use common_utils::{ext_traits::StringExt, id_type}; use diesel_models::enums as storage_enums; use masking::Secret; use time::OffsetDateTime; @@ -9,7 +9,7 @@ use crate::types::storage::dispute::Dispute; pub struct KafkaDispute<'a> { pub dispute_id: &'a String, pub dispute_amount: i64, - pub currency: &'a String, + pub currency: storage_enums::Currency, pub dispute_stage: &'a storage_enums::DisputeStage, pub dispute_status: &'a storage_enums::DisputeStatus, pub payment_id: &'a id_type::PaymentId, @@ -41,7 +41,13 @@ impl<'a> KafkaDispute<'a> { Self { dispute_id: &dispute.dispute_id, dispute_amount: dispute.amount.parse::().unwrap_or_default(), - currency: &dispute.currency, + currency: dispute.dispute_currency.unwrap_or( + dispute + .currency + .to_uppercase() + .parse_enum("Currency") + .unwrap_or_default(), + ), dispute_stage: &dispute.dispute_stage, dispute_status: &dispute.dispute_status, payment_id: &dispute.payment_id, diff --git a/crates/router/src/services/kafka/dispute_event.rs b/crates/router/src/services/kafka/dispute_event.rs index 057a060decd..64d91e8acaa 100644 --- a/crates/router/src/services/kafka/dispute_event.rs +++ b/crates/router/src/services/kafka/dispute_event.rs @@ -1,3 +1,4 @@ +use common_utils::ext_traits::StringExt; use diesel_models::enums as storage_enums; use masking::Secret; use time::OffsetDateTime; @@ -9,7 +10,7 @@ use crate::types::storage::dispute::Dispute; pub struct KafkaDisputeEvent<'a> { pub dispute_id: &'a String, pub dispute_amount: i64, - pub currency: &'a String, + pub currency: storage_enums::Currency, pub dispute_stage: &'a storage_enums::DisputeStage, pub dispute_status: &'a storage_enums::DisputeStatus, pub payment_id: &'a common_utils::id_type::PaymentId, @@ -41,7 +42,13 @@ impl<'a> KafkaDisputeEvent<'a> { Self { dispute_id: &dispute.dispute_id, dispute_amount: dispute.amount.parse::().unwrap_or_default(), - currency: &dispute.currency, + currency: dispute.dispute_currency.unwrap_or( + dispute + .currency + .to_uppercase() + .parse_enum("Currency") + .unwrap_or_default(), + ), dispute_stage: &dispute.dispute_stage, dispute_status: &dispute.dispute_status, payment_id: &dispute.payment_id, diff --git a/crates/router/src/types/storage/dispute.rs b/crates/router/src/types/storage/dispute.rs index 28a9573b333..2d42aaa0a8a 100644 --- a/crates/router/src/types/storage/dispute.rs +++ b/crates/router/src/types/storage/dispute.rs @@ -83,14 +83,8 @@ impl DisputeDbExt for Dispute { if let Some(dispute_status) = &dispute_list_constraints.dispute_status { filter = filter.filter(dsl::dispute_status.eq_any(dispute_status.clone())); } - if let Some(currency_list) = &dispute_list_constraints.currency { - let currency: Vec = currency_list - .iter() - .map(|currency| currency.to_string()) - .collect(); - - filter = filter.filter(dsl::currency.eq_any(currency)); + filter = filter.filter(dsl::dispute_currency.eq_any(currency_list.clone())); } if let Some(merchant_connector_id) = &dispute_list_constraints.merchant_connector_id { filter = filter.filter(dsl::merchant_connector_id.eq(merchant_connector_id.clone())) diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 8ba6783372b..8fe910cd7b4 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -886,7 +886,13 @@ impl ForeignFrom for api_models::disputes::DisputeResponse { payment_id: dispute.payment_id, attempt_id: dispute.attempt_id, amount: dispute.amount, - currency: dispute.currency, + currency: dispute.dispute_currency.unwrap_or( + dispute + .currency + .to_uppercase() + .parse_enum("Currency") + .unwrap_or_default(), + ), dispute_stage: dispute.dispute_stage, dispute_status: dispute.dispute_status, connector: dispute.connector, diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 4f859d8e56a..8ffb0f2001f 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -435,6 +435,7 @@ pub async fn generate_sample_data( merchant_connector_id: payment_attempt.merchant_connector_id.clone(), dispute_amount: amount * 100, organization_id: org_id.clone(), + dispute_currency: Some(payment_intent.currency.unwrap_or_default()), }) } else { None diff --git a/migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/down.sql b/migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/down.sql new file mode 100644 index 00000000000..052223ca0ae --- /dev/null +++ b/migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE dispute DROP COLUMN IF EXISTS dispute_currency; \ No newline at end of file diff --git a/migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/up.sql b/migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/up.sql new file mode 100644 index 00000000000..732909adfea --- /dev/null +++ b/migrations/2024-10-28-125949_add_dispute_currency_column_in_dispute_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE dispute ADD COLUMN IF NOT EXISTS dispute_currency "Currency"; \ No newline at end of file From 0f563b069994f47bba1ba77c79fef6307f3760e8 Mon Sep 17 00:00:00 2001 From: Jagan Date: Wed, 20 Nov 2024 19:14:03 +0530 Subject: [PATCH 05/14] feat(email): Add SMTP support to allow mails through self hosted/custom SMTP server (#6617) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 359 +++++++++++++++++- config/development.toml | 2 +- config/docker_compose.toml | 2 +- crates/external_services/Cargo.toml | 1 + crates/external_services/src/email.rs | 42 +- .../external_services/src/email/no_email.rs | 37 ++ crates/external_services/src/email/ses.rs | 37 +- crates/external_services/src/email/smtp.rs | 189 +++++++++ crates/router/src/configs/settings.rs | 4 + crates/router/src/routes/app.rs | 31 +- loadtest/config/development.toml | 2 +- 11 files changed, 670 insertions(+), 36 deletions(-) create mode 100644 crates/external_services/src/email/no_email.rs create mode 100644 crates/external_services/src/email/smtp.rs diff --git a/Cargo.lock b/Cargo.lock index 27b4158ba61..a2ede8c08c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1888,9 +1888,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.18" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -1963,6 +1963,16 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -2962,6 +2972,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email-encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" +dependencies = [ + "base64 0.22.1", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -3134,6 +3160,7 @@ dependencies = [ "hyper-proxy", "hyper-util", "hyperswitch_interfaces", + "lettre", "masking", "once_cell", "prost 0.13.2", @@ -3732,6 +3759,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "windows", +] + [[package]] name = "hsdev" version = "0.1.0" @@ -4117,6 +4155,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec 1.13.2", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -4133,6 +4289,27 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec 1.13.2", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "ignore" version = "0.4.22" @@ -4456,6 +4633,31 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "lettre" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" +dependencies = [ + "base64 0.22.1", + "chumsky", + "email-encoding", + "email_address", + "fastrand 2.1.1", + "futures-util", + "hostname", + "httpdate", + "idna 1.0.3", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "socket2", + "tokio 1.40.0", + "url", +] + [[package]] name = "libc" version = "0.2.158" @@ -4531,6 +4733,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "local-channel" version = "0.1.5" @@ -5832,7 +6040,7 @@ checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" dependencies = [ "bytes 1.7.1", "heck 0.5.0", - "itertools 0.12.1", + "itertools 0.13.0", "log", "multimap", "once_cell", @@ -5865,7 +6073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.77", @@ -5880,6 +6088,15 @@ dependencies = [ "prost 0.13.2", ] +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -5949,6 +6166,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + [[package]] name = "r2d2" version = "0.8.10" @@ -7669,6 +7892,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -8092,6 +8328,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -8939,7 +9185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -8956,6 +9202,18 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -9001,7 +9259,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da339118f018cc70ebf01fafc103360528aad53717e4bf311db929cb01cb9345" dependencies = [ - "idna", + "idna 0.5.0", "once_cell", "regex", "serde", @@ -9290,6 +9548,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -9529,6 +9797,18 @@ dependencies = [ "url", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -9580,6 +9860,30 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "synstructure 0.13.1", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -9601,12 +9905,55 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "synstructure 0.13.1", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "zstd" version = "0.13.2" diff --git a/config/development.toml b/config/development.toml index 6a6fda6755e..97739c3f5cd 100644 --- a/config/development.toml +++ b/config/development.toml @@ -311,7 +311,7 @@ wildcard_origin = true sender_email = "example@example.com" aws_region = "" allowed_unverified_days = 1 -active_email_client = "SES" +active_email_client = "NO_EMAIL_CLIENT" recon_recipient_email = "recon@example.com" prod_intent_recipient_email = "business@example.com" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 7adeee8a376..d71be958486 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -676,7 +676,7 @@ connector_list = "cybersource" sender_email = "example@example.com" # Sender email aws_region = "" # AWS region used by AWS SES allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email -active_email_client = "SES" # The currently active email client +active_email_client = "NO_EMAIL_CLIENT" # The currently active email client recon_recipient_email = "recon@example.com" # Recipient email for recon request email prod_intent_recipient_email = "business@example.com" # Recipient email for prod intent email diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 12b9dd3b0f3..e617dbe8351 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -29,6 +29,7 @@ error-stack = "0.4.1" hex = "0.4.3" hyper = "0.14.28" hyper-proxy = "0.9.1" +lettre = "0.11.10" once_cell = "1.19.0" serde = { version = "1.0.197", features = ["derive"] } thiserror = "1.0.58" diff --git a/crates/external_services/src/email.rs b/crates/external_services/src/email.rs index 5751de95c12..2e05a26e1f1 100644 --- a/crates/external_services/src/email.rs +++ b/crates/external_services/src/email.rs @@ -7,6 +7,12 @@ use serde::Deserialize; /// Implementation of aws ses client pub mod ses; +/// Implementation of SMTP server client +pub mod smtp; + +/// Implementation of Email client when email support is disabled +pub mod no_email; + /// Custom Result type alias for Email operations. pub type EmailResult = CustomResult; @@ -114,14 +120,27 @@ dyn_clone::clone_trait_object!(EmailClient); /// List of available email clients to choose from #[derive(Debug, Clone, Default, Deserialize)] -pub enum AvailableEmailClients { +#[serde(tag = "active_email_client")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum EmailClientConfigs { #[default] + /// Default Email client to use when no client is specified + NoEmailClient, /// AWS ses email client - SES, + Ses { + /// AWS SES client configuration + aws_ses: ses::SESConfig, + }, + /// Other Simple SMTP server + Smtp { + /// SMTP server configuration + smtp: smtp::SmtpServerConfig, + }, } /// Struct that contains the settings required to construct an EmailClient. #[derive(Debug, Clone, Default, Deserialize)] +#[serde(default)] pub struct EmailSettings { /// The AWS region to send SES requests to. pub aws_region: String, @@ -132,11 +151,9 @@ pub struct EmailSettings { /// Sender email pub sender_email: String, - /// Configs related to AWS Simple Email Service - pub aws_ses: Option, - - /// The active email client to use - pub active_email_client: AvailableEmailClients, + #[serde(flatten)] + /// The client specific configurations + pub client_config: EmailClientConfigs, /// Recipient email for recon emails pub recon_recipient_email: pii::Email, @@ -145,6 +162,17 @@ pub struct EmailSettings { pub prod_intent_recipient_email: pii::Email, } +impl EmailSettings { + /// Validation for the Email client specific configurations + pub fn validate(&self) -> Result<(), &'static str> { + match &self.client_config { + EmailClientConfigs::Ses { ref aws_ses } => aws_ses.validate(), + EmailClientConfigs::Smtp { ref smtp } => smtp.validate(), + EmailClientConfigs::NoEmailClient => Ok(()), + } + } +} + /// Errors that could occur from EmailClient. #[derive(Debug, thiserror::Error)] pub enum EmailError { diff --git a/crates/external_services/src/email/no_email.rs b/crates/external_services/src/email/no_email.rs new file mode 100644 index 00000000000..6ec5d69e1ab --- /dev/null +++ b/crates/external_services/src/email/no_email.rs @@ -0,0 +1,37 @@ +use common_utils::{errors::CustomResult, pii}; +use router_env::logger; + +use crate::email::{EmailClient, EmailError, EmailResult, IntermediateString}; + +/// Client when email support is disabled +#[derive(Debug, Clone, Default, serde::Deserialize)] +pub struct NoEmailClient {} + +impl NoEmailClient { + /// Constructs a new client when email is disabled + pub async fn create() -> Self { + Self {} + } +} + +#[async_trait::async_trait] +impl EmailClient for NoEmailClient { + type RichText = String; + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult { + Ok(intermediate_string.into_inner()) + } + + async fn send_email( + &self, + _recipient: pii::Email, + _subject: String, + _body: Self::RichText, + _proxy_url: Option<&String>, + ) -> EmailResult<()> { + logger::info!("Email not sent as email support is disabled, please enable any of the supported email clients to send emails"); + Ok(()) + } +} diff --git a/crates/external_services/src/email/ses.rs b/crates/external_services/src/email/ses.rs index 73599b344cd..f9dcc8f26ad 100644 --- a/crates/external_services/src/email/ses.rs +++ b/crates/external_services/src/email/ses.rs @@ -7,7 +7,7 @@ use aws_sdk_sesv2::{ Client, }; use aws_sdk_sts::config::Credentials; -use common_utils::{errors::CustomResult, ext_traits::OptionExt, pii}; +use common_utils::{errors::CustomResult, pii}; use error_stack::{report, ResultExt}; use hyper::Uri; use masking::PeekInterface; @@ -19,6 +19,7 @@ use crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, Intermed #[derive(Debug, Clone)] pub struct AwsSes { sender: String, + ses_config: SESConfig, settings: EmailSettings, } @@ -32,6 +33,21 @@ pub struct SESConfig { pub sts_role_session_name: String, } +impl SESConfig { + /// Validation for the SES client specific configs + pub fn validate(&self) -> Result<(), &'static str> { + use common_utils::{ext_traits::ConfigExt, fp_utils::when}; + + when(self.email_role_arn.is_default_or_empty(), || { + Err("email.aws_ses.email_role_arn must not be empty") + })?; + + when(self.sts_role_session_name.is_default_or_empty(), || { + Err("email.aws_ses.sts_role_session_name must not be empty") + }) + } +} + /// Errors that could occur during SES operations. #[derive(Debug, thiserror::Error)] pub enum AwsSesError { @@ -67,15 +83,20 @@ pub enum AwsSesError { impl AwsSes { /// Constructs a new AwsSes client - pub async fn create(conf: &EmailSettings, proxy_url: Option>) -> Self { + pub async fn create( + conf: &EmailSettings, + ses_config: &SESConfig, + proxy_url: Option>, + ) -> Self { // Build the client initially which will help us know if the email configuration is correct - Self::create_client(conf, proxy_url) + Self::create_client(conf, ses_config, proxy_url) .await .map_err(|error| logger::error!(?error, "Failed to initialize SES Client")) .ok(); Self { sender: conf.sender_email.clone(), + ses_config: ses_config.clone(), settings: conf.clone(), } } @@ -83,19 +104,13 @@ impl AwsSes { /// A helper function to create ses client pub async fn create_client( conf: &EmailSettings, + ses_config: &SESConfig, proxy_url: Option>, ) -> CustomResult { let sts_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url.as_ref())? .load() .await; - let ses_config = conf - .aws_ses - .as_ref() - .get_required_value("aws ses configuration") - .attach_printable("The selected email client is aws ses, but configuration is missing") - .change_context(AwsSesError::MissingConfigurationVariable("aws_ses"))?; - let role = aws_sdk_sts::Client::new(&sts_config) .assume_role() .role_arn(&ses_config.email_role_arn) @@ -219,7 +234,7 @@ impl EmailClient for AwsSes { ) -> EmailResult<()> { // Not using the same email client which was created at startup as the role session would expire // Create a client every time when the email is being sent - let email_client = Self::create_client(&self.settings, proxy_url) + let email_client = Self::create_client(&self.settings, &self.ses_config, proxy_url) .await .change_context(EmailError::ClientBuildingFailure)?; diff --git a/crates/external_services/src/email/smtp.rs b/crates/external_services/src/email/smtp.rs new file mode 100644 index 00000000000..33ab89f4571 --- /dev/null +++ b/crates/external_services/src/email/smtp.rs @@ -0,0 +1,189 @@ +use std::time::Duration; + +use common_utils::{errors::CustomResult, pii}; +use error_stack::ResultExt; +use lettre::{ + address::AddressError, + error, + message::{header::ContentType, Mailbox}, + transport::smtp::{self, authentication::Credentials}, + Message, SmtpTransport, Transport, +}; +use masking::{PeekInterface, Secret}; + +use crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, IntermediateString}; + +/// Client for SMTP server operation +#[derive(Debug, Clone, Default, serde::Deserialize)] +pub struct SmtpServer { + /// sender email id + pub sender: String, + /// SMTP server specific configs + pub smtp_config: SmtpServerConfig, +} + +impl SmtpServer { + /// A helper function to create SMTP server client + pub fn create_client(&self) -> Result { + let host = self.smtp_config.host.clone(); + let port = self.smtp_config.port; + let timeout = Some(Duration::from_secs(self.smtp_config.timeout)); + let credentials = self + .smtp_config + .username + .clone() + .zip(self.smtp_config.password.clone()) + .map(|(username, password)| { + Credentials::new(username.peek().to_owned(), password.peek().to_owned()) + }); + match &self.smtp_config.connection { + SmtpConnection::StartTls => match credentials { + Some(credentials) => Ok(SmtpTransport::starttls_relay(&host) + .map_err(SmtpError::ConnectionFailure)? + .port(port) + .timeout(timeout) + .credentials(credentials) + .build()), + None => Ok(SmtpTransport::starttls_relay(&host) + .map_err(SmtpError::ConnectionFailure)? + .port(port) + .timeout(timeout) + .build()), + }, + SmtpConnection::Plaintext => match credentials { + Some(credentials) => Ok(SmtpTransport::builder_dangerous(&host) + .port(port) + .timeout(timeout) + .credentials(credentials) + .build()), + None => Ok(SmtpTransport::builder_dangerous(&host) + .port(port) + .timeout(timeout) + .build()), + }, + } + } + /// Constructs a new SMTP client + pub async fn create(conf: &EmailSettings, smtp_config: SmtpServerConfig) -> Self { + Self { + sender: conf.sender_email.clone(), + smtp_config: smtp_config.clone(), + } + } + /// helper function to convert email id into Mailbox + fn to_mail_box(email: String) -> EmailResult { + Ok(Mailbox::new( + None, + email + .parse() + .map_err(SmtpError::EmailParsingFailed) + .change_context(EmailError::EmailSendingFailure)?, + )) + } +} +/// Struct that contains the SMTP server specific configs required +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct SmtpServerConfig { + /// hostname of the SMTP server eg: smtp.gmail.com + pub host: String, + /// portname of the SMTP server eg: 25 + pub port: u16, + /// timeout for the SMTP server connection in seconds eg: 10 + pub timeout: u64, + /// Username name of the SMTP server + pub username: Option>, + /// Password of the SMTP server + pub password: Option>, + /// Connection type of the SMTP server + #[serde(default)] + pub connection: SmtpConnection, +} + +/// Enum that contains the connection types of the SMTP server +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SmtpConnection { + #[default] + /// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS + StartTls, + /// Plaintext connection (very insecure) + Plaintext, +} + +impl SmtpServerConfig { + /// Validation for the SMTP server client specific configs + pub fn validate(&self) -> Result<(), &'static str> { + use common_utils::{ext_traits::ConfigExt, fp_utils::when}; + when(self.host.is_default_or_empty(), || { + Err("email.smtp.host must not be empty") + })?; + self.username.clone().zip(self.password.clone()).map_or( + Ok(()), + |(username, password)| { + when(username.peek().is_default_or_empty(), || { + Err("email.smtp.username must not be empty") + })?; + when(password.peek().is_default_or_empty(), || { + Err("email.smtp.password must not be empty") + }) + }, + )?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl EmailClient for SmtpServer { + type RichText = String; + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult { + Ok(intermediate_string.into_inner()) + } + + async fn send_email( + &self, + recipient: pii::Email, + subject: String, + body: Self::RichText, + _proxy_url: Option<&String>, + ) -> EmailResult<()> { + // Create a client every time when the email is being sent + let email_client = + Self::create_client(self).change_context(EmailError::EmailSendingFailure)?; + + let email = Message::builder() + .to(Self::to_mail_box(recipient.peek().to_string())?) + .from(Self::to_mail_box(self.sender.clone())?) + .subject(subject) + .header(ContentType::TEXT_HTML) + .body(body) + .map_err(SmtpError::MessageBuildingFailed) + .change_context(EmailError::EmailSendingFailure)?; + + email_client + .send(&email) + .map_err(SmtpError::SendingFailure) + .change_context(EmailError::EmailSendingFailure)?; + Ok(()) + } +} + +/// Errors that could occur during SES operations. +#[derive(Debug, thiserror::Error)] +pub enum SmtpError { + /// An error occurred in the SMTP while sending email. + #[error("Failed to Send Email {0:?}")] + SendingFailure(smtp::Error), + /// An error occurred in the SMTP while building the message content. + #[error("Failed to create connection {0:?}")] + ConnectionFailure(smtp::Error), + /// An error occurred in the SMTP while building the message content. + #[error("Failed to Build Email content {0:?}")] + MessageBuildingFailed(error::Error), + /// An error occurred in the SMTP while building the message content. + #[error("Failed to parse given email {0:?}")] + EmailParsingFailed(AddressError), +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f675aad11a7..76b58f5b67b 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -881,6 +881,10 @@ impl Settings { .transpose()?; self.key_manager.get_inner().validate()?; + #[cfg(feature = "email")] + self.email + .validate() + .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?; Ok(()) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 1d7e9727cf3..baa2ba4ae15 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -8,7 +8,9 @@ use common_enums::TransactionType; #[cfg(feature = "partial-auth")] use common_utils::crypto::Blake3; #[cfg(feature = "email")] -use external_services::email::{ses::AwsSes, EmailService}; +use external_services::email::{ + no_email::NoEmailClient, ses::AwsSes, smtp::SmtpServer, EmailClientConfigs, EmailService, +}; use external_services::{file_storage::FileStorageInterface, grpc_client::GrpcClients}; use hyperswitch_interfaces::{ encryption_interface::EncryptionManagementInterface, @@ -97,7 +99,7 @@ pub struct SessionState { pub api_client: Box, pub event_handler: EventsHandler, #[cfg(feature = "email")] - pub email_client: Arc, + pub email_client: Arc>, #[cfg(feature = "olap")] pub pool: AnalyticsProvider, pub file_storage_client: Arc, @@ -195,7 +197,7 @@ pub struct AppState { pub conf: Arc>, pub event_handler: EventsHandler, #[cfg(feature = "email")] - pub email_client: Arc, + pub email_client: Arc>, pub api_client: Box, #[cfg(feature = "olap")] pub pools: HashMap, @@ -215,7 +217,7 @@ pub trait AppStateInfo { fn conf(&self) -> settings::Settings; fn event_handler(&self) -> EventsHandler; #[cfg(feature = "email")] - fn email_client(&self) -> Arc; + fn email_client(&self) -> Arc>; fn add_request_id(&mut self, request_id: RequestId); fn add_flow_name(&mut self, flow_name: String); fn get_request_id(&self) -> Option; @@ -232,7 +234,7 @@ impl AppStateInfo for AppState { self.conf.as_ref().to_owned() } #[cfg(feature = "email")] - fn email_client(&self) -> Arc { + fn email_client(&self) -> Arc> { self.email_client.to_owned() } fn event_handler(&self) -> EventsHandler { @@ -258,11 +260,22 @@ impl AsRef for AppState { } #[cfg(feature = "email")] -pub async fn create_email_client(settings: &settings::Settings) -> impl EmailService { - match settings.email.active_email_client { - external_services::email::AvailableEmailClients::SES => { - AwsSes::create(&settings.email, settings.proxy.https_url.to_owned()).await +pub async fn create_email_client( + settings: &settings::Settings, +) -> Box { + match &settings.email.client_config { + EmailClientConfigs::Ses { aws_ses } => Box::new( + AwsSes::create( + &settings.email, + aws_ses, + settings.proxy.https_url.to_owned(), + ) + .await, + ), + EmailClientConfigs::Smtp { smtp } => { + Box::new(SmtpServer::create(&settings.email, smtp.clone()).await) } + EmailClientConfigs::NoEmailClient => Box::new(NoEmailClient::create().await), } } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 934b072f14a..288e72164cf 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -384,7 +384,7 @@ public = { base_url = "http://localhost:8080", schema = "public", redis_key_pref sender_email = "example@example.com" aws_region = "" allowed_unverified_days = 1 -active_email_client = "SES" +active_email_client = "NO_EMAIL_CLIENT" recon_recipient_email = "recon@example.com" prod_intent_recipient_email = "business@example.com" From 5611769964e372eb4690ef95ce950a2842f074d3 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:14:36 +0530 Subject: [PATCH 06/14] refactor(router): remove metadata, additional_merchant_data and connector_wallets_details from connector list api (#6583) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 21 --------- api-reference/openapi_spec.json | 21 --------- crates/api_models/src/admin.rs | 22 ---------- crates/router/src/types/transformers.rs | 52 ----------------------- cypress-tests/cypress/support/commands.js | 5 +++ 5 files changed, 5 insertions(+), 116 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index c6a1fad0d35..661e51f6c18 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -10020,11 +10020,6 @@ ], "nullable": true }, - "metadata": { - "type": "object", - "description": "Metadata is useful for storing additional, unstructured information on an object.", - "nullable": true - }, "disabled": { "type": "boolean", "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", @@ -10055,22 +10050,6 @@ }, "status": { "$ref": "#/components/schemas/ConnectorStatus" - }, - "additional_merchant_data": { - "allOf": [ - { - "$ref": "#/components/schemas/AdditionalMerchantData" - } - ], - "nullable": true - }, - "connector_wallets_details": { - "allOf": [ - { - "$ref": "#/components/schemas/ConnectorWalletDetails" - } - ], - "nullable": true } }, "additionalProperties": false diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 353e9d5d49b..801b32b490d 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -13159,11 +13159,6 @@ ], "nullable": true }, - "metadata": { - "type": "object", - "description": "Metadata is useful for storing additional, unstructured information on an object.", - "nullable": true - }, "test_mode": { "type": "boolean", "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", @@ -13221,22 +13216,6 @@ }, "status": { "$ref": "#/components/schemas/ConnectorStatus" - }, - "additional_merchant_data": { - "allOf": [ - { - "$ref": "#/components/schemas/AdditionalMerchantData" - } - ], - "nullable": true - }, - "connector_wallets_details": { - "allOf": [ - { - "$ref": "#/components/schemas/ConnectorWalletDetails" - } - ], - "nullable": true } }, "additionalProperties": false diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index d80a4ea11b4..8ba50649236 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1313,10 +1313,6 @@ pub struct MerchantConnectorListResponse { ]))] pub payment_methods_enabled: Option>, - /// Metadata is useful for storing additional, unstructured information on an object. - #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] - pub metadata: Option, - /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. #[schema(default = false, example = false)] pub test_mode: Option, @@ -1349,13 +1345,6 @@ pub struct MerchantConnectorListResponse { #[schema(value_type = ConnectorStatus, example = "inactive")] pub status: api_enums::ConnectorStatus, - - #[schema(value_type = Option)] - pub additional_merchant_data: Option, - - /// The connector_wallets_details is used to store wallet details such as certificates and wallet credentials - #[schema(value_type = Option)] - pub connector_wallets_details: Option, } #[cfg(feature = "v1")] @@ -1423,10 +1412,6 @@ pub struct MerchantConnectorListResponse { ]))] pub payment_methods_enabled: Option>, - /// Metadata is useful for storing additional, unstructured information on an object. - #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] - pub metadata: Option, - /// A boolean value to indicate if the connector is disabled. By default, its value is false. #[schema(default = false, example = false)] pub disabled: Option, @@ -1443,13 +1428,6 @@ pub struct MerchantConnectorListResponse { #[schema(value_type = ConnectorStatus, example = "inactive")] pub status: api_enums::ConnectorStatus, - - #[schema(value_type = Option)] - pub additional_merchant_data: Option, - - /// The connector_wallets_details is used to store wallet details such as certificates and wallet credentials - #[schema(value_type = Option)] - pub connector_wallets_details: Option, } #[cfg(feature = "v2")] diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 8fe910cd7b4..c47c0ca342c 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1028,7 +1028,6 @@ impl ForeignTryFrom test_mode: item.test_mode, disabled: item.disabled, payment_methods_enabled, - metadata: item.metadata, business_country: item.business_country, business_label: item.business_label, business_sub_label: item.business_sub_label, @@ -1037,31 +1036,6 @@ impl ForeignTryFrom applepay_verified_domains: item.applepay_verified_domains, pm_auth_config: item.pm_auth_config, status: item.status, - additional_merchant_data: item - .additional_merchant_data - .map(|data| { - let data = data.into_inner(); - serde_json::Value::parse_value::( - data.expose(), - "AdditionalMerchantData", - ) - .attach_printable("Unable to deserialize additional_merchant_data") - .change_context(errors::ApiErrorResponse::InternalServerError) - }) - .transpose()? - .map(api_models::admin::AdditionalMerchantData::foreign_from), - connector_wallets_details: item - .connector_wallets_details - .map(|data| { - data.into_inner() - .expose() - .parse_value::( - "ConnectorWalletDetails", - ) - .attach_printable("Unable to deserialize connector_wallets_details") - .change_context(errors::ApiErrorResponse::InternalServerError) - }) - .transpose()?, }; #[cfg(feature = "v2")] let response = Self { @@ -1071,37 +1045,11 @@ impl ForeignTryFrom connector_label: item.connector_label, disabled: item.disabled, payment_methods_enabled, - metadata: item.metadata, frm_configs, profile_id: item.profile_id, applepay_verified_domains: item.applepay_verified_domains, pm_auth_config: item.pm_auth_config, status: item.status, - additional_merchant_data: item - .additional_merchant_data - .map(|data| { - let data = data.into_inner(); - serde_json::Value::parse_value::( - data.expose(), - "AdditionalMerchantData", - ) - .attach_printable("Unable to deserialize additional_merchant_data") - .change_context(errors::ApiErrorResponse::InternalServerError) - }) - .transpose()? - .map(api_models::admin::AdditionalMerchantData::foreign_from), - connector_wallets_details: item - .connector_wallets_details - .map(|data| { - data.into_inner() - .expose() - .parse_value::( - "ConnectorWalletDetails", - ) - .attach_printable("Unable to deserialize connector_wallets_details") - .change_context(errors::ApiErrorResponse::InternalServerError) - }) - .transpose()?, }; Ok(response) } diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 6204c86f44a..5ed6617ae42 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -631,6 +631,11 @@ Cypress.Commands.add("connectorListByMid", (globalState) => { logRequestId(response.headers["x-request-id"]); expect(response.headers["content-type"]).to.include("application/json"); expect(response.body).to.be.an("array").and.not.empty; + response.body.forEach((item) => { + expect(item).to.not.have.property("metadata"); + expect(item).to.not.have.property("additional_merchant_data"); + expect(item).to.not.have.property("connector_wallets_details"); + }); }); }); From f24578310f44adf75c993ba42225554377399961 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:21:39 +0000 Subject: [PATCH 07/14] chore(version): 2024.11.21.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad0c05a08f..4e5135f104c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.21.0 + +### Features + +- **email:** Add SMTP support to allow mails through self hosted/custom SMTP server ([#6617](https://github.com/juspay/hyperswitch/pull/6617)) ([`0f563b0`](https://github.com/juspay/hyperswitch/commit/0f563b069994f47bba1ba77c79fef6307f3760e8)) +- **router:** Add support for network token migration ([#6300](https://github.com/juspay/hyperswitch/pull/6300)) ([`012e352`](https://github.com/juspay/hyperswitch/commit/012e352db0477f5ddb4429cb0e4f5d781fd901a7)) +- **users:** Convert emails to lowercase from requests ([#6601](https://github.com/juspay/hyperswitch/pull/6601)) ([`c04f81e`](https://github.com/juspay/hyperswitch/commit/c04f81e3c4362369a92b2ead5ee1b28b4ca44b52)) + +### Bug Fixes + +- **connector:** [Volt] handle 5xx error for Volt payments webhooks ([#6576](https://github.com/juspay/hyperswitch/pull/6576)) ([`75ec96b`](https://github.com/juspay/hyperswitch/commit/75ec96b6131d470b39171415058106b3464de75a)) +- **dispute:** Change dispute currency type to currency enum ([#6454](https://github.com/juspay/hyperswitch/pull/6454)) ([`98aa84b`](https://github.com/juspay/hyperswitch/commit/98aa84b7e842ac85ce2461f3eab826a6c3783832)) + +### Refactors + +- **router:** Remove metadata, additional_merchant_data and connector_wallets_details from connector list api ([#6583](https://github.com/juspay/hyperswitch/pull/6583)) ([`5611769`](https://github.com/juspay/hyperswitch/commit/5611769964e372eb4690ef95ce950a2842f074d3)) + +**Full Changelog:** [`2024.11.20.0...2024.11.21.0`](https://github.com/juspay/hyperswitch/compare/2024.11.20.0...2024.11.21.0) + +- - - + ## 2024.11.20.0 ### Features From 9bc363f140afcdc3d4dc624d6410a42c33afaeed Mon Sep 17 00:00:00 2001 From: AmeyWale-HS Date: Thu, 21 Nov 2024 23:35:31 +0530 Subject: [PATCH 08/14] feat(connector): [Xendit] Template PR (#6593) Co-authored-by: Amey Wale --- api-reference-v2/openapi_spec.json | 1 + api-reference/openapi_spec.json | 1 + config/config.example.toml | 2 + config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/connector_enums.rs | 2 + crates/common_enums/src/connector_enums.rs | 1 + crates/connector_configs/src/connector.rs | 1 + .../hyperswitch_connectors/src/connectors.rs | 3 +- .../src/connectors/xendit.rs | 563 ++++++++++++++++++ .../src/connectors/xendit/transformers.rs | 228 +++++++ .../src/default_implementations.rs | 32 + .../src/default_implementations_v2.rs | 22 + crates/hyperswitch_interfaces/src/configs.rs | 1 + crates/router/src/connector.rs | 2 +- crates/router/src/types/api.rs | 3 + crates/router/src/types/transformers.rs | 1 + crates/router/tests/connectors/xendit.rs | 402 +++++++++++++ crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + scripts/add_connector.sh | 2 +- 24 files changed, 1274 insertions(+), 3 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/xendit.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs create mode 100644 crates/router/tests/connectors/xendit.rs diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 661e51f6c18..095e386d981 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -18167,6 +18167,7 @@ "wise", "worldline", "worldpay", + "xendit", "zen", "plaid", "zsl" diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 801b32b490d..247b308bd65 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -22815,6 +22815,7 @@ "wise", "worldline", "worldpay", + "xendit", "zen", "plaid", "zsl" diff --git a/config/config.example.toml b/config/config.example.toml index 289087f4a33..08a32035c4b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -271,6 +271,7 @@ wellsfargopayout.base_url = "https://api-sandbox.wellsfargo.com/" wise.base_url = "https://api.sandbox.transferwise.tech/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +xendit.base_url = "https://api.xendit.co" zsl.base_url = "https://api.sitoffalb.net/" zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" @@ -329,6 +330,7 @@ cards = [ "threedsecureio", "thunes", "worldpay", + "xendit", "zen", "zsl", ] diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 1c7a40ea539..e1ea99bc8d1 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -111,6 +111,7 @@ wellsfargopayout.base_url = "https://api-sandbox.wellsfargo.com/" wise.base_url = "https://api.sandbox.transferwise.tech/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +xendit.base_url = "https://api.xendit.co" zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" zsl.base_url = "https://api.sitoffalb.net/" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 76f42085f92..58b6a49fed0 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -115,6 +115,7 @@ wellsfargopayout.base_url = "https://api.wellsfargo.com/" wise.base_url = "https://api.sandbox.transferwise.tech/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +xendit.base_url = "https://api.xendit.co" zen.base_url = "https://api.zen.com/" zen.secondary_base_url = "https://secure.zen.com/" zsl.base_url = "https://apirh.prodoffalb.net/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 98e1e7e00d9..4421d1b1e96 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -115,6 +115,7 @@ wellsfargopayout.base_url = "https://api-sandbox.wellsfargo.com/" wise.base_url = "https://api.sandbox.transferwise.tech/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +xendit.base_url = "https://api.xendit.co" zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" zsl.base_url = "https://api.sitoffalb.net/" diff --git a/config/development.toml b/config/development.toml index 97739c3f5cd..a58497f4a51 100644 --- a/config/development.toml +++ b/config/development.toml @@ -169,6 +169,7 @@ cards = [ "wise", "worldline", "worldpay", + "xendit", "zen", "zsl", ] @@ -279,6 +280,7 @@ stripe.base_url_file_upload = "https://files.stripe.com/" wise.base_url = "https://api.sandbox.transferwise.tech/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +xendit.base_url = "https://api.xendit.co" trustpay.base_url = "https://test-tpgw.trustpay.eu/" tsys.base_url = "https://stagegw.transnox.com/" volt.base_url = "https://api.sandbox.volt.io/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index d71be958486..c293a7ac821 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -201,6 +201,7 @@ wellsfargopayout.base_url = "https://api-sandbox.wellsfargo.com/" wise.base_url = "https://api.sandbox.transferwise.tech/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +xendit.base_url = "https://api.xendit.co" zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" zsl.base_url = "https://api.sitoffalb.net/" @@ -287,6 +288,7 @@ cards = [ "wise", "worldline", "worldpay", + "xendit", "zen", "zsl", ] diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 77081f49957..11b38400920 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -129,6 +129,7 @@ pub enum Connector { Signifyd, Plaid, Riskified, + // Xendit, Zen, Zsl, } @@ -259,6 +260,7 @@ impl Connector { | Self::Wise | Self::Worldline | Self::Worldpay + // | Self::Xendit | Self::Zen | Self::Zsl | Self::Signifyd diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 9beaed75960..e75a4038c07 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -123,6 +123,7 @@ pub enum RoutableConnectors { Wise, Worldline, Worldpay, + Xendit, Zen, Plaid, Zsl, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 3634fe730ca..b1cf0bd9f9c 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -231,6 +231,7 @@ pub struct ConnectorConfig { pub wise_payout: Option, pub worldline: Option, pub worldpay: Option, + pub xendit: Option, pub square: Option, pub stax: Option, pub dummy_connector: Option, diff --git a/crates/hyperswitch_connectors/src/connectors.rs b/crates/hyperswitch_connectors/src/connectors.rs index 2bc82450096..b764ab5b441 100644 --- a/crates/hyperswitch_connectors/src/connectors.rs +++ b/crates/hyperswitch_connectors/src/connectors.rs @@ -36,6 +36,7 @@ pub mod tsys; pub mod volt; pub mod worldline; pub mod worldpay; +pub mod xendit; pub mod zen; pub mod zsl; @@ -48,5 +49,5 @@ pub use self::{ nexinets::Nexinets, nexixpay::Nexixpay, nomupay::Nomupay, novalnet::Novalnet, payeezy::Payeezy, payu::Payu, powertranz::Powertranz, razorpay::Razorpay, shift4::Shift4, square::Square, stax::Stax, taxjar::Taxjar, thunes::Thunes, tsys::Tsys, volt::Volt, worldline::Worldline, - worldpay::Worldpay, zen::Zen, zsl::Zsl, + worldpay::Worldpay, xendit::Xendit, zen::Zen, zsl::Zsl, }; diff --git a/crates/hyperswitch_connectors/src/connectors/xendit.rs b/crates/hyperswitch_connectors/src/connectors/xendit.rs new file mode 100644 index 00000000000..c3f75a7673e --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/xendit.rs @@ -0,0 +1,563 @@ +pub mod transformers; + +use common_utils::{ + errors::CustomResult, + ext_traits::BytesExt, + request::{Method, Request, RequestBuilder, RequestContent}, + types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, +}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_flow_types::{ + access_token_auth::AccessTokenAuth, + payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, + refunds::{Execute, RSync}, + }, + router_request_types::{ + AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, + PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, + RefundsData, SetupMandateRequestData, + }, + router_response_types::{PaymentsResponseData, RefundsResponseData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, + RefundSyncRouterData, RefundsRouterData, + }, +}; +use hyperswitch_interfaces::{ + api::{self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorValidation}, + configs::Connectors, + errors, + events::connector_api_logs::ConnectorEvent, + types::{self, Response}, + webhooks, +}; +use masking::{ExposeInterface, Mask}; +use transformers as xendit; + +use crate::{constants::headers, types::ResponseRouterData, utils}; + +#[derive(Clone)] +pub struct Xendit { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Xendit { + pub fn new() -> &'static Self { + &Self { + amount_converter: &StringMinorUnitForConnector, + } + } +} + +impl api::Payment for Xendit {} +impl api::PaymentSession for Xendit {} +impl api::ConnectorAccessToken for Xendit {} +impl api::MandateSetup for Xendit {} +impl api::PaymentAuthorize for Xendit {} +impl api::PaymentSync for Xendit {} +impl api::PaymentCapture for Xendit {} +impl api::PaymentVoid for Xendit {} +impl api::Refund for Xendit {} +impl api::RefundExecute for Xendit {} +impl api::RefundSync for Xendit {} +impl api::PaymentToken for Xendit {} + +impl ConnectorIntegration + for Xendit +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Xendit +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Xendit { + fn id(&self) -> &'static str { + "xendit" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + // TODO! Check connector documentation, on which unit they are processing the currency. + // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, + // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + connectors.xendit.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = xendit::XenditAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.expose().into_masked(), + )]) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: xendit::XenditErrorResponse = res + .response + .parse_struct("XenditErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl ConnectorValidation for Xendit { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration for Xendit { + //TODO: implement sessions flow +} + +impl ConnectorIntegration for Xendit {} + +impl ConnectorIntegration for Xendit {} + +impl ConnectorIntegration for Xendit { + fn get_headers( + &self, + req: &PaymentsAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.currency, + )?; + + let connector_router_data = xendit::XenditRouterData::from((amount, req)); + let connector_req = xendit::XenditPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsAuthorizeRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: xendit::XenditPaymentsResponse = res + .response + .parse_struct("Xendit PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Xendit { + fn get_headers( + &self, + req: &PaymentsSyncRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &PaymentsSyncRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsSyncRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: xendit::XenditPaymentsResponse = res + .response + .parse_struct("xendit PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Xendit { + fn get_headers( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + fn build_request( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCaptureRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: xendit::XenditPaymentsResponse = res + .response + .parse_struct("Xendit PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Xendit {} + +impl ConnectorIntegration for Xendit { + fn get_headers( + &self, + req: &RefundsRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &RefundsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &RefundsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let refund_amount = utils::convert_amount( + self.amount_converter, + req.request.minor_refund_amount, + req.request.currency, + )?; + + let connector_router_data = xendit::XenditRouterData::from((refund_amount, req)); + let connector_req = xendit::XenditRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &RefundsRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &RefundsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: xendit::RefundResponse = + res.response + .parse_struct("xendit RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Xendit { + fn get_headers( + &self, + req: &RefundSyncRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &RefundSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &RefundSyncRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .set_body(types::RefundSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RefundSyncRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: xendit::RefundResponse = res + .response + .parse_struct("xendit RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +#[async_trait::async_trait] +impl webhooks::IncomingWebhook for Xendit { + fn get_webhook_object_reference_id( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_event_type( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_resource_object( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs b/crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs new file mode 100644 index 00000000000..c9d4cd2583c --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/xendit/transformers.rs @@ -0,0 +1,228 @@ +use common_enums::enums; +use common_utils::types::StringMinorUnit; +use hyperswitch_domain_models::{ + payment_method_data::PaymentMethodData, + router_data::{ConnectorAuthType, RouterData}, + router_flow_types::refunds::{Execute, RSync}, + router_request_types::ResponseId, + router_response_types::{PaymentsResponseData, RefundsResponseData}, + types::{PaymentsAuthorizeRouterData, RefundsRouterData}, +}; +use hyperswitch_interfaces::errors; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + types::{RefundsResponseRouterData, ResponseRouterData}, + utils::PaymentsAuthorizeRequestData, +}; + +//TODO: Fill the struct with respective fields +pub struct XenditRouterData { + pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl From<(StringMinorUnit, T)> for XenditRouterData { + fn from((amount, item): (StringMinorUnit, T)) -> Self { + //Todo : use utils to convert the amount to the type of amount that a connector accepts + Self { + amount, + router_data: item, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct XenditPaymentsRequest { + amount: StringMinorUnit, + card: XenditCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct XenditCard { + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&XenditRouterData<&PaymentsAuthorizeRouterData>> for XenditPaymentsRequest { + type Error = error_stack::Report; + fn try_from( + item: &XenditRouterData<&PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(req_card) => { + let card = XenditCard { + number: req_card.card_number, + expiry_month: req_card.card_exp_month, + expiry_year: req_card.card_exp_year, + cvc: req_card.card_cvc, + complete: item.router_data.request.is_auto_capture()?, + }; + Ok(Self { + amount: item.amount.clone(), + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), + } + } +} + +//TODO: Fill the struct with respective fields +// Auth Struct +pub struct XenditAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&ConnectorAuthType> for XenditAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &ConnectorAuthType) -> Result { + match auth_type { + ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} +// PaymentsResponse +//TODO: Append the remaining status flags +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum XenditPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for common_enums::AttemptStatus { + fn from(item: XenditPaymentStatus) -> Self { + match item { + XenditPaymentStatus::Succeeded => Self::Charged, + XenditPaymentStatus::Failed => Self::Failure, + XenditPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct XenditPaymentsResponse { + status: XenditPaymentStatus, + id: String, +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + status: common_enums::AttemptStatus::from(item.response.status), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct XenditRefundRequest { + pub amount: StringMinorUnit, +} + +impl TryFrom<&XenditRouterData<&RefundsRouterData>> for XenditRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &XenditRouterData<&RefundsRouterData>) -> Result { + Ok(Self { + amount: item.amount.to_owned(), + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + //TODO: Review mapping + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +impl TryFrom> for RefundsRouterData { + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +impl TryFrom> for RefundsRouterData { + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct XenditErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 6ee8926f8df..bc86e713501 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -126,6 +126,7 @@ default_imp_for_authorize_session_token!( connectors::Tsys, connectors::Worldline, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -182,6 +183,7 @@ default_imp_for_calculate_tax!( connectors::Volt, connectors::Worldline, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -234,6 +236,7 @@ default_imp_for_session_update!( connectors::Globepay, connectors::Worldline, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl, connectors::Powertranz, @@ -291,6 +294,7 @@ default_imp_for_post_session_tokens!( connectors::Globepay, connectors::Worldline, connectors::Worldpay, + connectors::Xendit, connectors::Powertranz, connectors::Thunes, connectors::Tsys, @@ -346,6 +350,7 @@ default_imp_for_complete_authorize!( connectors::Tsys, connectors::Worldline, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -404,6 +409,7 @@ default_imp_for_incremental_authorization!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -461,6 +467,7 @@ default_imp_for_create_customer!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -515,6 +522,7 @@ default_imp_for_connector_redirect_response!( connectors::Tsys, connectors::Worldline, connectors::Volt, + connectors::Xendit, connectors::Zsl ); @@ -569,6 +577,7 @@ default_imp_for_pre_processing_steps!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -627,6 +636,7 @@ default_imp_for_post_processing_steps!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -685,6 +695,7 @@ default_imp_for_approve!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -743,6 +754,7 @@ default_imp_for_reject!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -801,6 +813,7 @@ default_imp_for_webhook_source_verification!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -860,6 +873,7 @@ default_imp_for_accept_dispute!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -918,6 +932,7 @@ default_imp_for_submit_evidence!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -976,6 +991,7 @@ default_imp_for_defend_dispute!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1043,6 +1059,7 @@ default_imp_for_file_upload!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1093,6 +1110,7 @@ default_imp_for_payouts!( connectors::Volt, connectors::Worldline, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1153,6 +1171,7 @@ default_imp_for_payouts_create!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1213,6 +1232,7 @@ default_imp_for_payouts_retrieve!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1273,6 +1293,7 @@ default_imp_for_payouts_eligibility!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1333,6 +1354,7 @@ default_imp_for_payouts_fulfill!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1393,6 +1415,7 @@ default_imp_for_payouts_cancel!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1453,6 +1476,7 @@ default_imp_for_payouts_quote!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1513,6 +1537,7 @@ default_imp_for_payouts_recipient!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1573,6 +1598,7 @@ default_imp_for_payouts_recipient_account!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1633,6 +1659,7 @@ default_imp_for_frm_sale!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1693,6 +1720,7 @@ default_imp_for_frm_checkout!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1753,6 +1781,7 @@ default_imp_for_frm_transaction!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1813,6 +1842,7 @@ default_imp_for_frm_fulfillment!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1873,6 +1903,7 @@ default_imp_for_frm_record_return!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1930,6 +1961,7 @@ default_imp_for_revoking_mandates!( connectors::Worldline, connectors::Worldpay, connectors::Volt, + connectors::Xendit, connectors::Zen, connectors::Zsl ); diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index d747d65d73a..75cbf192e55 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -243,6 +243,7 @@ default_imp_for_new_connector_integration_payment!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -302,6 +303,7 @@ default_imp_for_new_connector_integration_refund!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -356,6 +358,7 @@ default_imp_for_new_connector_integration_connector_access_token!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -416,6 +419,7 @@ default_imp_for_new_connector_integration_accept_dispute!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -475,6 +479,7 @@ default_imp_for_new_connector_integration_submit_evidence!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -534,6 +539,7 @@ default_imp_for_new_connector_integration_defend_dispute!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -603,6 +609,7 @@ default_imp_for_new_connector_integration_file_upload!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -664,6 +671,7 @@ default_imp_for_new_connector_integration_payouts_create!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -725,6 +733,7 @@ default_imp_for_new_connector_integration_payouts_eligibility!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -786,6 +795,7 @@ default_imp_for_new_connector_integration_payouts_fulfill!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -847,6 +857,7 @@ default_imp_for_new_connector_integration_payouts_cancel!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -908,6 +919,7 @@ default_imp_for_new_connector_integration_payouts_quote!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -969,6 +981,7 @@ default_imp_for_new_connector_integration_payouts_recipient!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1030,6 +1043,7 @@ default_imp_for_new_connector_integration_payouts_sync!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1091,6 +1105,7 @@ default_imp_for_new_connector_integration_payouts_recipient_account!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1150,6 +1165,7 @@ default_imp_for_new_connector_integration_webhook_source_verification!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1211,6 +1227,7 @@ default_imp_for_new_connector_integration_frm_sale!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1272,6 +1289,7 @@ default_imp_for_new_connector_integration_frm_checkout!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1333,6 +1351,7 @@ default_imp_for_new_connector_integration_frm_transaction!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1394,6 +1413,7 @@ default_imp_for_new_connector_integration_frm_fulfillment!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1455,6 +1475,7 @@ default_imp_for_new_connector_integration_frm_record_return!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); @@ -1513,6 +1534,7 @@ default_imp_for_new_connector_integration_revoking_mandates!( connectors::Worldline, connectors::Volt, connectors::Worldpay, + connectors::Xendit, connectors::Zen, connectors::Zsl ); diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index 58810a0cb34..5e5eeea31b3 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -92,6 +92,7 @@ pub struct Connectors { pub wise: ConnectorParams, pub worldline: ConnectorParams, pub worldpay: ConnectorParams, + pub xendit: ConnectorParams, pub zen: ConnectorParams, pub zsl: ConnectorParams, } diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 7913d6543c8..e98730d006d 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -57,7 +57,7 @@ pub use hyperswitch_connectors::connectors::{ powertranz::Powertranz, razorpay, razorpay::Razorpay, shift4, shift4::Shift4, square, square::Square, stax, stax::Stax, taxjar, taxjar::Taxjar, thunes, thunes::Thunes, tsys, tsys::Tsys, volt, volt::Volt, worldline, worldline::Worldline, worldpay, worldpay::Worldpay, - zen, zen::Zen, zsl, zsl::Zsl, + xendit, xendit::Xendit, zen, zen::Zen, zsl, zsl::Zsl, }; #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index b432c57c5d1..f7909a97195 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -496,6 +496,9 @@ impl ConnectorData { enums::Connector::Worldpay => { Ok(ConnectorEnum::Old(Box::new(connector::Worldpay::new()))) } + // enums::Connector::Xendit => { + // Ok(ConnectorEnum::Old(Box::new(connector::Xendit::new()))) + // } enums::Connector::Mifinity => { Ok(ConnectorEnum::Old(Box::new(connector::Mifinity::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index c47c0ca342c..ecec8211446 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -303,6 +303,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Wise => Self::Wise, api_enums::Connector::Worldline => Self::Worldline, api_enums::Connector::Worldpay => Self::Worldpay, + // api_enums::Connector::Xendit => Self::Xendit, api_enums::Connector::Zen => Self::Zen, api_enums::Connector::Zsl => Self::Zsl, #[cfg(feature = "dummy_connector")] diff --git a/crates/router/tests/connectors/xendit.rs b/crates/router/tests/connectors/xendit.rs new file mode 100644 index 00000000000..452f22a1122 --- /dev/null +++ b/crates/router/tests/connectors/xendit.rs @@ -0,0 +1,402 @@ +use masking::Secret; +use router::{ + types::{self, api, storage::enums, +}}; + +use crate::utils::{self, ConnectorActions}; +use test_utils::connector_auth; + +#[derive(Clone, Copy)] +struct XenditTest; +impl ConnectorActions for XenditTest {} +impl utils::Connector for XenditTest { + fn get_data(&self) -> api::ConnectorData { + use router::connector::Xendit; + api::ConnectorData { + connector: Box::new(Xendit::new()), + connector_name: types::Connector::Xendit, + get_token: types::api::GetToken::Connector, + merchant_connector_id: None, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .xendit + .expect("Missing connector authentication configuration").into(), + ) + } + + fn get_name(&self) -> String { + "xendit".to_string() + } +} + +static CONNECTOR: XenditTest = XenditTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// 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(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). +#[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). +#[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: 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). +#[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: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + 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). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + 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). +#[actix_web::test] +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). +#[actix_web::test] +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). +#[actix_web::test] +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). +#[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). +#[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: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .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(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). +#[actix_web::test] +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). +#[actix_web::test] +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). +#[actix_web::test] +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 scenarios +// 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::PaymentMethodData::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, + "Your card's security code 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: api::PaymentMethodData::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, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: api::PaymentMethodData::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, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// 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(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, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// 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, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// 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( + 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/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index be02e5720fb..2b2dc143113 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -92,6 +92,7 @@ pub struct ConnectorAuthentication { // pub wellsfargopayout: Option, pub wise: Option, pub worldpay: Option, + pub xendit: Option, pub worldline: Option, pub zen: Option, pub zsl: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 288e72164cf..b7a6fca7de4 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -166,6 +166,7 @@ wellsfargo.base_url = "https://apitest.cybersource.com/" wellsfargopayout.base_url = "https://api-sandbox.wellsfargo.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +xendit.base_url = "https://api.xendit.co" wise.base_url = "https://api.sandbox.transferwise.tech/" zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" @@ -253,6 +254,7 @@ cards = [ "wise", "worldline", "worldpay", + "xendit", "zen", "zsl", ] diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 0ed38e8ef5d..9f1992bfa44 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen adyenplatform airwallex amazonpay applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay itaubank jpmorgan klarna mifinity mollie multisafepay netcetera nexinets nexixpay nomupay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys volt wellsfargo wellsfargopayout wise worldline worldpay zsl "$1") + connectors=(aci adyen adyenplatform airwallex amazonpay applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay itaubank jpmorgan klarna mifinity mollie multisafepay netcetera nexinets nexixpay nomupay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys volt wellsfargo wellsfargopayout wise worldline worldpay xendit zsl "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res="$(echo ${sorted[@]})" sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp From bf13c16109d0113f900c806b0722895a36ec2d5a Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:36:54 +0530 Subject: [PATCH 09/14] feat(themes): Add `theme_name` and `entity_type` in themes table (#6621) --- crates/common_utils/src/types/theme.rs | 11 ++++++----- crates/diesel_models/src/query/user/theme.rs | 19 ++++++++++--------- crates/diesel_models/src/schema.rs | 4 ++++ crates/diesel_models/src/schema_v2.rs | 4 ++++ crates/diesel_models/src/user/theme.rs | 5 +++++ crates/router/src/db/user/theme.rs | 15 +++++++++------ .../down.sql | 3 +++ .../up.sql | 3 +++ 8 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/down.sql create mode 100644 migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/up.sql diff --git a/crates/common_utils/src/types/theme.rs b/crates/common_utils/src/types/theme.rs index a2e6fe4b19c..03b4cf23a6c 100644 --- a/crates/common_utils/src/types/theme.rs +++ b/crates/common_utils/src/types/theme.rs @@ -4,11 +4,12 @@ use crate::id_type; /// Currently being used for theme related APIs and queries. #[derive(Debug)] pub enum ThemeLineage { - /// Tenant lineage variant - Tenant { - /// tenant_id: String - tenant_id: String, - }, + // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType + // /// Tenant lineage variant + // Tenant { + // /// tenant_id: String + // tenant_id: String, + // }, /// Org lineage variant Organization { /// tenant_id: String diff --git a/crates/diesel_models/src/query/user/theme.rs b/crates/diesel_models/src/query/user/theme.rs index c021edca325..78fd5025ef9 100644 --- a/crates/diesel_models/src/query/user/theme.rs +++ b/crates/diesel_models/src/query/user/theme.rs @@ -3,7 +3,7 @@ use diesel::{ associations::HasTable, pg::Pg, sql_types::{Bool, Nullable}, - BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, + BoolExpressionMethods, ExpressionMethods, }; use crate::{ @@ -27,14 +27,15 @@ impl Theme { + 'static, > { match lineage { - ThemeLineage::Tenant { tenant_id } => Box::new( - dsl::tenant_id - .eq(tenant_id) - .and(dsl::org_id.is_null()) - .and(dsl::merchant_id.is_null()) - .and(dsl::profile_id.is_null()) - .nullable(), - ), + // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType + // ThemeLineage::Tenant { tenant_id } => Box::new( + // dsl::tenant_id + // .eq(tenant_id) + // .and(dsl::org_id.is_null()) + // .and(dsl::merchant_id.is_null()) + // .and(dsl::profile_id.is_null()) + // .nullable(), + // ), ThemeLineage::Organization { tenant_id, org_id } => Box::new( dsl::tenant_id .eq(tenant_id) diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 2b815db390c..6dddbb754d5 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1280,6 +1280,10 @@ diesel::table! { profile_id -> Nullable, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + entity_type -> Varchar, + #[max_length = 64] + theme_name -> Varchar, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index aea22e3d7c6..d44dd3317d3 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1227,6 +1227,10 @@ diesel::table! { profile_id -> Nullable, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + entity_type -> Varchar, + #[max_length = 64] + theme_name -> Varchar, } } diff --git a/crates/diesel_models/src/user/theme.rs b/crates/diesel_models/src/user/theme.rs index 0824ae71919..2f8152e419c 100644 --- a/crates/diesel_models/src/user/theme.rs +++ b/crates/diesel_models/src/user/theme.rs @@ -1,3 +1,4 @@ +use common_enums::EntityType; use common_utils::id_type; use diesel::{Identifiable, Insertable, Queryable, Selectable}; use time::PrimitiveDateTime; @@ -14,6 +15,8 @@ pub struct Theme { pub profile_id: Option, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, + pub entity_type: EntityType, + pub theme_name: String, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -26,4 +29,6 @@ pub struct ThemeNew { pub profile_id: Option, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, + pub entity_type: EntityType, + pub theme_name: String, } diff --git a/crates/router/src/db/user/theme.rs b/crates/router/src/db/user/theme.rs index d71b82cdea4..f1e4f4f794e 100644 --- a/crates/router/src/db/user/theme.rs +++ b/crates/router/src/db/user/theme.rs @@ -65,12 +65,13 @@ impl ThemeInterface for Store { fn check_theme_with_lineage(theme: &storage::Theme, lineage: &ThemeLineage) -> bool { match lineage { - ThemeLineage::Tenant { tenant_id } => { - &theme.tenant_id == tenant_id - && theme.org_id.is_none() - && theme.merchant_id.is_none() - && theme.profile_id.is_none() - } + // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType + // ThemeLineage::Tenant { tenant_id } => { + // &theme.tenant_id == tenant_id + // && theme.org_id.is_none() + // && theme.merchant_id.is_none() + // && theme.profile_id.is_none() + // } ThemeLineage::Organization { tenant_id, org_id } => { &theme.tenant_id == tenant_id && theme @@ -156,6 +157,8 @@ impl ThemeInterface for MockDb { profile_id: new_theme.profile_id, created_at: new_theme.created_at, last_modified_at: new_theme.last_modified_at, + entity_type: new_theme.entity_type, + theme_name: new_theme.theme_name, }; themes.push(theme.clone()); diff --git a/migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/down.sql b/migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/down.sql new file mode 100644 index 00000000000..a56426b60c0 --- /dev/null +++ b/migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE themes DROP COLUMN IF EXISTS entity_type; +ALTER TABLE themes DROP COLUMN IF EXISTS theme_name; diff --git a/migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/up.sql b/migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/up.sql new file mode 100644 index 00000000000..924385d45d6 --- /dev/null +++ b/migrations/2024-11-20-110014_add-entity-type-and-theme-name-in-themes/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE themes ADD COLUMN IF NOT EXISTS entity_type VARCHAR(64) NOT NULL; +ALTER TABLE themes ADD COLUMN IF NOT EXISTS theme_name VARCHAR(64) NOT NULL; From bc65a848a14c1e5c8a50cf4bf5764a7af2918ac9 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:37:34 +0530 Subject: [PATCH 10/14] feat(connector): [AIRWALLEX] Update production endpoint (#6632) --- config/deployments/production.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 58b6a49fed0..e5f90646058 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -28,7 +28,7 @@ adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpaymen adyen.payout_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/" adyen.dispute_base_url = "https://{{merchant_endpoint_prefix}}-ca-live.adyen.com/" adyenplatform.base_url = "https://balanceplatform-api-live.adyen.com/" -airwallex.base_url = "https://api-demo.airwallex.com/" +airwallex.base_url = "https://api.airwallex.com/" amazonpay.base_url = "https://pay-api.amazon.com/v2" applepay.base_url = "https://apple-pay-gateway.apple.com/" authorizedotnet.base_url = "https://api.authorize.net/xml/v1/request.api" From 9010214c6e62a65f91e0eeca6d5f21468e5c63aa Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:30:46 +0530 Subject: [PATCH 11/14] fix(connector): [Novalnet] Get email from customer email if billing.email is not present (#6619) --- .../src/connectors/novalnet/transformers.rs | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index f9bbe8398cb..b2bf76944c0 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -4,11 +4,7 @@ use api_models::webhooks::IncomingWebhookEvent; use cards::CardNumber; use common_enums::{enums, enums as api_enums}; use common_utils::{ - consts, - ext_traits::OptionExt, - pii::{Email, IpAddress}, - request::Method, - types::StringMinorUnit, + consts, ext_traits::OptionExt, pii::Email, request::Method, types::StringMinorUnit, }; use error_stack::ResultExt; use hyperswitch_domain_models::{ @@ -32,9 +28,8 @@ use strum::Display; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, utils::{ - self, ApplePay, BrowserInformationData, PaymentsAuthorizeRequestData, - PaymentsCancelRequestData, PaymentsCaptureRequestData, PaymentsSyncRequestData, - RefundsRequestData, RouterData as _, + self, ApplePay, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, + PaymentsCaptureRequestData, PaymentsSyncRequestData, RefundsRequestData, RouterData as _, }, }; @@ -82,7 +77,6 @@ pub struct NovalnetPaymentsRequestCustomer { email: Email, mobile: Option>, billing: Option, - customer_ip: Option>, no_nc: i64, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] @@ -194,20 +188,15 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym country_code: item.router_data.get_optional_billing_country(), }; - let customer_ip = item - .router_data - .request - .get_browser_info()? - .get_ip_address() - .ok(); - let customer = NovalnetPaymentsRequestCustomer { first_name: item.router_data.get_billing_first_name()?, last_name: item.router_data.get_billing_last_name()?, - email: item.router_data.get_billing_email()?, + email: item + .router_data + .get_billing_email() + .or(item.router_data.request.get_email())?, mobile: item.router_data.get_optional_billing_phone_number(), billing: Some(billing), - customer_ip, // no_nc is used to indicate if minimal customer data is passed or not no_nc: 1, }; From 40d3c38b830a7163331778064d0e1917d30fc17e Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 22 Nov 2024 02:59:37 +0530 Subject: [PATCH 12/14] refactor: update API response for JSON deserialization errors (#6610) --- crates/router/src/utils.rs | 6 ++++- .../cypress/e2e/configs/Payment/Commons.js | 27 ++++++++++++++----- .../cypress/e2e/configs/Payment/Utils.js | 11 +++++--- .../cypress/e2e/PaymentUtils/Commons.js | 27 ++++++++++++++----- .../cypress/e2e/PaymentUtils/Paybox.js | 27 ++++++++++++++----- .../cypress/e2e/PaymentUtils/Utils.js | 11 +++++--- 6 files changed, 81 insertions(+), 28 deletions(-) diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 515c9a94ce8..ca61543ec57 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -81,7 +81,11 @@ pub mod error_parser { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str( serde_json::to_string(&serde_json::json!({ - "error": self.err.to_string() + "error": { + "error_type": "invalid_request", + "message": self.err.to_string(), + "code": "IR_06", + } })) .as_deref() .unwrap_or("Invalid Json Error"), diff --git a/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js b/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js index c2d1b7483c1..d445dbd26cd 100644 --- a/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js +++ b/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js @@ -964,7 +964,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: "Json deserialize error: invalid card number length", + error: { + error_type: "invalid_request", + message: "Json deserialize error: invalid card number length", + code: "IR_06" + }, }, }, }, @@ -1068,8 +1072,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `United`, expected one of `AED`, `AFN`, `ALL`, `AMD`, `ANG`, `AOA`, `ARS`, `AUD`, `AWG`, `AZN`, `BAM`, `BBD`, `BDT`, `BGN`, `BHD`, `BIF`, `BMD`, `BND`, `BOB`, `BRL`, `BSD`, `BTN`, `BWP`, `BYN`, `BZD`, `CAD`, `CDF`, `CHF`, `CLP`, `CNY`, `COP`, `CRC`, `CUP`, `CVE`, `CZK`, `DJF`, `DKK`, `DOP`, `DZD`, `EGP`, `ERN`, `ETB`, `EUR`, `FJD`, `FKP`, `GBP`, `GEL`, `GHS`, `GIP`, `GMD`, `GNF`, `GTQ`, `GYD`, `HKD`, `HNL`, `HRK`, `HTG`, `HUF`, `IDR`, `ILS`, `INR`, `IQD`, `IRR`, `ISK`, `JMD`, `JOD`, `JPY`, `KES`, `KGS`, `KHR`, `KMF`, `KPW`, `KRW`, `KWD`, `KYD`, `KZT`, `LAK`, `LBP`, `LKR`, `LRD`, `LSL`, `LYD`, `MAD`, `MDL`, `MGA`, `MKD`, `MMK`, `MNT`, `MOP`, `MRU`, `MUR`, `MVR`, `MWK`, `MXN`, `MYR`, `MZN`, `NAD`, `NGN`, `NIO`, `NOK`, `NPR`, `NZD`, `OMR`, `PAB`, `PEN`, `PGK`, `PHP`, `PKR`, `PLN`, `PYG`, `QAR`, `RON`, `RSD`, `RUB`, `RWF`, `SAR`, `SBD`, `SCR`, `SDG`, `SEK`, `SGD`, `SHP`, `SLE`, `SLL`, `SOS`, `SRD`, `SSP`, `STN`, `SVC`, `SYP`, `SZL`, `THB`, `TJS`, `TMT`, `TND`, `TOP`, `TRY`, `TTD`, `TWD`, `TZS`, `UAH`, `UGX`, `USD`, `UYU`, `UZS`, `VES`, `VND`, `VUV`, `WST`, `XAF`, `XCD`, `XOF`, `XPF`, `YER`, `ZAR`, `ZMW`, `ZWL`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `United`, expected one of `AED`, `AFN`, `ALL`, `AMD`, `ANG`, `AOA`, `ARS`, `AUD`, `AWG`, `AZN`, `BAM`, `BBD`, `BDT`, `BGN`, `BHD`, `BIF`, `BMD`, `BND`, `BOB`, `BRL`, `BSD`, `BTN`, `BWP`, `BYN`, `BZD`, `CAD`, `CDF`, `CHF`, `CLP`, `CNY`, `COP`, `CRC`, `CUP`, `CVE`, `CZK`, `DJF`, `DKK`, `DOP`, `DZD`, `EGP`, `ERN`, `ETB`, `EUR`, `FJD`, `FKP`, `GBP`, `GEL`, `GHS`, `GIP`, `GMD`, `GNF`, `GTQ`, `GYD`, `HKD`, `HNL`, `HRK`, `HTG`, `HUF`, `IDR`, `ILS`, `INR`, `IQD`, `IRR`, `ISK`, `JMD`, `JOD`, `JPY`, `KES`, `KGS`, `KHR`, `KMF`, `KPW`, `KRW`, `KWD`, `KYD`, `KZT`, `LAK`, `LBP`, `LKR`, `LRD`, `LSL`, `LYD`, `MAD`, `MDL`, `MGA`, `MKD`, `MMK`, `MNT`, `MOP`, `MRU`, `MUR`, `MVR`, `MWK`, `MXN`, `MYR`, `MZN`, `NAD`, `NGN`, `NIO`, `NOK`, `NPR`, `NZD`, `OMR`, `PAB`, `PEN`, `PGK`, `PHP`, `PKR`, `PLN`, `PYG`, `QAR`, `RON`, `RSD`, `RUB`, `RWF`, `SAR`, `SBD`, `SCR`, `SDG`, `SEK`, `SGD`, `SHP`, `SLE`, `SLL`, `SOS`, `SRD`, `SSP`, `STN`, `SVC`, `SYP`, `SZL`, `THB`, `TJS`, `TMT`, `TND`, `TOP`, `TRY`, `TTD`, `TWD`, `TZS`, `UAH`, `UGX`, `USD`, `UYU`, `UZS`, `VES`, `VND`, `VUV`, `WST`, `XAF`, `XCD`, `XOF`, `XPF`, `YER`, `ZAR`, `ZMW`, `ZWL`", + code: "IR_06" + }, }, }, }, @@ -1093,8 +1100,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `auto`, expected one of `automatic`, `manual`, `manual_multiple`, `scheduled`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `auto`, expected one of `automatic`, `manual`, `manual_multiple`, `scheduled`", + code: "IR_06" + }, }, }, }, @@ -1117,8 +1127,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `this_supposed_to_be_a_card`, expected one of `card`, `card_redirect`, `pay_later`, `wallet`, `bank_redirect`, `bank_transfer`, `crypto`, `bank_debit`, `reward`, `real_time_payment`, `upi`, `voucher`, `gift_card`, `open_banking`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `this_supposed_to_be_a_card`, expected one of `card`, `card_redirect`, `pay_later`, `wallet`, `bank_redirect`, `bank_transfer`, `crypto`, `bank_debit`, `reward`, `real_time_payment`, `upi`, `voucher`, `gift_card`, `open_banking`, `mobile_payment`", + code: "IR_06" + }, }, }, }, diff --git a/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js b/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js index 9785839f422..569b557b690 100644 --- a/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js +++ b/cypress-tests-v2/cypress/e2e/configs/Payment/Utils.js @@ -88,9 +88,14 @@ export function defaultErrorHandler(response, response_data) { if (typeof response.body.error === "object") { for (const key in response_data.body.error) { - expect(response_data.body.error[key]).to.equal(response.body.error[key]); + // Check if the error message is a Json deserialize error + let apiResponseContent = response.body.error[key]; + let expectedContent = response_data.body.error[key]; + if (typeof apiResponseContent === "string" && apiResponseContent.includes("Json deserialize error")) { + expect(apiResponseContent).to.include(expectedContent); + } else { + expect(apiResponseContent).to.equal(expectedContent); + } } - } else if (typeof response.body.error === "string") { - expect(response.body.error).to.include(response_data.body.error); } } diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js index 41de9bf2ff4..e601c1fb95e 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js @@ -1071,7 +1071,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: "Json deserialize error: invalid card number length", + error: { + error_type: "invalid_request", + message: "Json deserialize error: invalid card number length", + code: "IR_06" + }, }, }, }, @@ -1175,8 +1179,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `United`, expected one of `AED`, `AFN`, `ALL`, `AMD`, `ANG`, `AOA`, `ARS`, `AUD`, `AWG`, `AZN`, `BAM`, `BBD`, `BDT`, `BGN`, `BHD`, `BIF`, `BMD`, `BND`, `BOB`, `BRL`, `BSD`, `BTN`, `BWP`, `BYN`, `BZD`, `CAD`, `CDF`, `CHF`, `CLP`, `CNY`, `COP`, `CRC`, `CUP`, `CVE`, `CZK`, `DJF`, `DKK`, `DOP`, `DZD`, `EGP`, `ERN`, `ETB`, `EUR`, `FJD`, `FKP`, `GBP`, `GEL`, `GHS`, `GIP`, `GMD`, `GNF`, `GTQ`, `GYD`, `HKD`, `HNL`, `HRK`, `HTG`, `HUF`, `IDR`, `ILS`, `INR`, `IQD`, `IRR`, `ISK`, `JMD`, `JOD`, `JPY`, `KES`, `KGS`, `KHR`, `KMF`, `KPW`, `KRW`, `KWD`, `KYD`, `KZT`, `LAK`, `LBP`, `LKR`, `LRD`, `LSL`, `LYD`, `MAD`, `MDL`, `MGA`, `MKD`, `MMK`, `MNT`, `MOP`, `MRU`, `MUR`, `MVR`, `MWK`, `MXN`, `MYR`, `MZN`, `NAD`, `NGN`, `NIO`, `NOK`, `NPR`, `NZD`, `OMR`, `PAB`, `PEN`, `PGK`, `PHP`, `PKR`, `PLN`, `PYG`, `QAR`, `RON`, `RSD`, `RUB`, `RWF`, `SAR`, `SBD`, `SCR`, `SDG`, `SEK`, `SGD`, `SHP`, `SLE`, `SLL`, `SOS`, `SRD`, `SSP`, `STN`, `SVC`, `SYP`, `SZL`, `THB`, `TJS`, `TMT`, `TND`, `TOP`, `TRY`, `TTD`, `TWD`, `TZS`, `UAH`, `UGX`, `USD`, `UYU`, `UZS`, `VES`, `VND`, `VUV`, `WST`, `XAF`, `XCD`, `XOF`, `XPF`, `YER`, `ZAR`, `ZMW`, `ZWL`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `United`, expected one of `AED`, `AFN`, `ALL`, `AMD`, `ANG`, `AOA`, `ARS`, `AUD`, `AWG`, `AZN`, `BAM`, `BBD`, `BDT`, `BGN`, `BHD`, `BIF`, `BMD`, `BND`, `BOB`, `BRL`, `BSD`, `BTN`, `BWP`, `BYN`, `BZD`, `CAD`, `CDF`, `CHF`, `CLP`, `CNY`, `COP`, `CRC`, `CUP`, `CVE`, `CZK`, `DJF`, `DKK`, `DOP`, `DZD`, `EGP`, `ERN`, `ETB`, `EUR`, `FJD`, `FKP`, `GBP`, `GEL`, `GHS`, `GIP`, `GMD`, `GNF`, `GTQ`, `GYD`, `HKD`, `HNL`, `HRK`, `HTG`, `HUF`, `IDR`, `ILS`, `INR`, `IQD`, `IRR`, `ISK`, `JMD`, `JOD`, `JPY`, `KES`, `KGS`, `KHR`, `KMF`, `KPW`, `KRW`, `KWD`, `KYD`, `KZT`, `LAK`, `LBP`, `LKR`, `LRD`, `LSL`, `LYD`, `MAD`, `MDL`, `MGA`, `MKD`, `MMK`, `MNT`, `MOP`, `MRU`, `MUR`, `MVR`, `MWK`, `MXN`, `MYR`, `MZN`, `NAD`, `NGN`, `NIO`, `NOK`, `NPR`, `NZD`, `OMR`, `PAB`, `PEN`, `PGK`, `PHP`, `PKR`, `PLN`, `PYG`, `QAR`, `RON`, `RSD`, `RUB`, `RWF`, `SAR`, `SBD`, `SCR`, `SDG`, `SEK`, `SGD`, `SHP`, `SLE`, `SLL`, `SOS`, `SRD`, `SSP`, `STN`, `SVC`, `SYP`, `SZL`, `THB`, `TJS`, `TMT`, `TND`, `TOP`, `TRY`, `TTD`, `TWD`, `TZS`, `UAH`, `UGX`, `USD`, `UYU`, `UZS`, `VES`, `VND`, `VUV`, `WST`, `XAF`, `XCD`, `XOF`, `XPF`, `YER`, `ZAR`, `ZMW`, `ZWL`", + code: "IR_06" + }, }, }, }, @@ -1200,8 +1207,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `auto`, expected one of `automatic`, `manual`, `manual_multiple`, `scheduled`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `auto`, expected one of `automatic`, `manual`, `manual_multiple`, `scheduled`", + code: "IR_06" + }, }, }, }, @@ -1224,8 +1234,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `this_supposed_to_be_a_card`, expected one of `card`, `card_redirect`, `pay_later`, `wallet`, `bank_redirect`, `bank_transfer`, `crypto`, `bank_debit`, `reward`, `real_time_payment`, `upi`, `voucher`, `gift_card`, `open_banking`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `this_supposed_to_be_a_card`, expected one of `card`, `card_redirect`, `pay_later`, `wallet`, `bank_redirect`, `bank_transfer`, `crypto`, `bank_debit`, `reward`, `real_time_payment`, `upi`, `voucher`, `gift_card`, `open_banking`, `mobile_payment`", + code: "IR_06" + }, }, }, }, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js index b0550854ef8..d1eb91692da 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js @@ -151,7 +151,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: "Json deserialize error: invalid card number length", + error: { + error_type: "invalid_request", + message: "Json deserialize error: invalid card number length", + code: "IR_06" + }, }, }, }, @@ -255,8 +259,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `United`, expected one of `AED`, `AFN`, `ALL`, `AMD`, `ANG`, `AOA`, `ARS`, `AUD`, `AWG`, `AZN`, `BAM`, `BBD`, `BDT`, `BGN`, `BHD`, `BIF`, `BMD`, `BND`, `BOB`, `BRL`, `BSD`, `BTN`, `BWP`, `BYN`, `BZD`, `CAD`, `CDF`, `CHF`, `CLP`, `CNY`, `COP`, `CRC`, `CUP`, `CVE`, `CZK`, `DJF`, `DKK`, `DOP`, `DZD`, `EGP`, `ERN`, `ETB`, `EUR`, `FJD`, `FKP`, `GBP`, `GEL`, `GHS`, `GIP`, `GMD`, `GNF`, `GTQ`, `GYD`, `HKD`, `HNL`, `HRK`, `HTG`, `HUF`, `IDR`, `ILS`, `INR`, `IQD`, `IRR`, `ISK`, `JMD`, `JOD`, `JPY`, `KES`, `KGS`, `KHR`, `KMF`, `KPW`, `KRW`, `KWD`, `KYD`, `KZT`, `LAK`, `LBP`, `LKR`, `LRD`, `LSL`, `LYD`, `MAD`, `MDL`, `MGA`, `MKD`, `MMK`, `MNT`, `MOP`, `MRU`, `MUR`, `MVR`, `MWK`, `MXN`, `MYR`, `MZN`, `NAD`, `NGN`, `NIO`, `NOK`, `NPR`, `NZD`, `OMR`, `PAB`, `PEN`, `PGK`, `PHP`, `PKR`, `PLN`, `PYG`, `QAR`, `RON`, `RSD`, `RUB`, `RWF`, `SAR`, `SBD`, `SCR`, `SDG`, `SEK`, `SGD`, `SHP`, `SLE`, `SLL`, `SOS`, `SRD`, `SSP`, `STN`, `SVC`, `SYP`, `SZL`, `THB`, `TJS`, `TMT`, `TND`, `TOP`, `TRY`, `TTD`, `TWD`, `TZS`, `UAH`, `UGX`, `USD`, `UYU`, `UZS`, `VES`, `VND`, `VUV`, `WST`, `XAF`, `XCD`, `XOF`, `XPF`, `YER`, `ZAR`, `ZMW`, `ZWL`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `United`, expected one of `AED`, `AFN`, `ALL`, `AMD`, `ANG`, `AOA`, `ARS`, `AUD`, `AWG`, `AZN`, `BAM`, `BBD`, `BDT`, `BGN`, `BHD`, `BIF`, `BMD`, `BND`, `BOB`, `BRL`, `BSD`, `BTN`, `BWP`, `BYN`, `BZD`, `CAD`, `CDF`, `CHF`, `CLP`, `CNY`, `COP`, `CRC`, `CUP`, `CVE`, `CZK`, `DJF`, `DKK`, `DOP`, `DZD`, `EGP`, `ERN`, `ETB`, `EUR`, `FJD`, `FKP`, `GBP`, `GEL`, `GHS`, `GIP`, `GMD`, `GNF`, `GTQ`, `GYD`, `HKD`, `HNL`, `HRK`, `HTG`, `HUF`, `IDR`, `ILS`, `INR`, `IQD`, `IRR`, `ISK`, `JMD`, `JOD`, `JPY`, `KES`, `KGS`, `KHR`, `KMF`, `KPW`, `KRW`, `KWD`, `KYD`, `KZT`, `LAK`, `LBP`, `LKR`, `LRD`, `LSL`, `LYD`, `MAD`, `MDL`, `MGA`, `MKD`, `MMK`, `MNT`, `MOP`, `MRU`, `MUR`, `MVR`, `MWK`, `MXN`, `MYR`, `MZN`, `NAD`, `NGN`, `NIO`, `NOK`, `NPR`, `NZD`, `OMR`, `PAB`, `PEN`, `PGK`, `PHP`, `PKR`, `PLN`, `PYG`, `QAR`, `RON`, `RSD`, `RUB`, `RWF`, `SAR`, `SBD`, `SCR`, `SDG`, `SEK`, `SGD`, `SHP`, `SLE`, `SLL`, `SOS`, `SRD`, `SSP`, `STN`, `SVC`, `SYP`, `SZL`, `THB`, `TJS`, `TMT`, `TND`, `TOP`, `TRY`, `TTD`, `TWD`, `TZS`, `UAH`, `UGX`, `USD`, `UYU`, `UZS`, `VES`, `VND`, `VUV`, `WST`, `XAF`, `XCD`, `XOF`, `XPF`, `YER`, `ZAR`, `ZMW`, `ZWL`", + code: "IR_06" + }, }, }, }, @@ -280,8 +287,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `auto`, expected one of `automatic`, `manual`, `manual_multiple`, `scheduled`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `auto`, expected one of `automatic`, `manual`, `manual_multiple`, `scheduled`", + code: "IR_06" + }, }, }, }, @@ -304,8 +314,11 @@ export const connectorDetails = { Response: { status: 400, body: { - error: - "Json deserialize error: unknown variant `this_supposed_to_be_a_card`, expected one of `card`, `card_redirect`, `pay_later`, `wallet`, `bank_redirect`, `bank_transfer`, `crypto`, `bank_debit`, `reward`, `real_time_payment`, `upi`, `voucher`, `gift_card`, `open_banking`", + error: { + error_type: "invalid_request", + message: "Json deserialize error: unknown variant `this_supposed_to_be_a_card`, expected one of `card`, `card_redirect`, `pay_later`, `wallet`, `bank_redirect`, `bank_transfer`, `crypto`, `bank_debit`, `reward`, `real_time_payment`, `upi`, `voucher`, `gift_card`, `open_banking`, `mobile_payment`", + code: "IR_06" + }, }, }, }, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index 618f5864f6d..399dc2b15a3 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -134,9 +134,14 @@ export function defaultErrorHandler(response, response_data) { if (typeof response.body.error === "object") { for (const key in response_data.body.error) { - expect(response_data.body.error[key]).to.equal(response.body.error[key]); + // Check if the error message is a Json deserialize error + let apiResponseContent = response.body.error[key]; + let expectedContent = response_data.body.error[key]; + if (typeof apiResponseContent === "string" && apiResponseContent.includes("Json deserialize error")) { + expect(apiResponseContent).to.include(expectedContent); + } else { + expect(apiResponseContent).to.equal(expectedContent); + } } - } else if (typeof response.body.error === "string") { - expect(response.body.error).to.include(response_data.body.error); } } From 500fcd38145e5ba35cd87580429008364068f8c3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:22:16 +0000 Subject: [PATCH 13/14] chore(version): 2024.11.22.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e5135f104c..eee4bf49472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.22.0 + +### Features + +- **connector:** + - [Xendit] Template PR ([#6593](https://github.com/juspay/hyperswitch/pull/6593)) ([`9bc363f`](https://github.com/juspay/hyperswitch/commit/9bc363f140afcdc3d4dc624d6410a42c33afaeed)) + - [AIRWALLEX] Update production endpoint ([#6632](https://github.com/juspay/hyperswitch/pull/6632)) ([`bc65a84`](https://github.com/juspay/hyperswitch/commit/bc65a848a14c1e5c8a50cf4bf5764a7af2918ac9)) +- **themes:** Add `theme_name` and `entity_type` in themes table ([#6621](https://github.com/juspay/hyperswitch/pull/6621)) ([`bf13c16`](https://github.com/juspay/hyperswitch/commit/bf13c16109d0113f900c806b0722895a36ec2d5a)) + +### Bug Fixes + +- **connector:** [Novalnet] Get email from customer email if billing.email is not present ([#6619](https://github.com/juspay/hyperswitch/pull/6619)) ([`9010214`](https://github.com/juspay/hyperswitch/commit/9010214c6e62a65f91e0eeca6d5f21468e5c63aa)) + +### Refactors + +- Update API response for JSON deserialization errors ([#6610](https://github.com/juspay/hyperswitch/pull/6610)) ([`40d3c38`](https://github.com/juspay/hyperswitch/commit/40d3c38b830a7163331778064d0e1917d30fc17e)) + +**Full Changelog:** [`2024.11.21.0...2024.11.22.0`](https://github.com/juspay/hyperswitch/compare/2024.11.21.0...2024.11.22.0) + +- - - + ## 2024.11.21.0 ### Features From 54e393bf9a55bdc4527a723b7a03968f21848a5e Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:49:32 +0530 Subject: [PATCH 14/14] fix(analytics): remove first_attempt group by in Payment Intent old metrics (#6627) --- .../payment_intents/metrics/payment_processed_amount.rs | 9 --------- .../src/payment_intents/metrics/payments_success_rate.rs | 9 --------- .../sessionized_metrics/payment_processed_amount.rs | 6 +++--- .../metrics/sessionized_metrics/payments_distribution.rs | 2 +- .../metrics/sessionized_metrics/payments_success_rate.rs | 2 +- .../metrics/sessionized_metrics/smart_retried_amount.rs | 4 ++-- .../src/payment_intents/metrics/smart_retried_amount.rs | 7 ------- 7 files changed, 7 insertions(+), 32 deletions(-) diff --git a/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs b/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs index cf733b0c3da..696dd6a584b 100644 --- a/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/payment_processed_amount.rs @@ -59,10 +59,6 @@ where }) .switch()?; - query_builder - .add_select_column("attempt_count == 1 as first_attempt") - .switch()?; - query_builder.add_select_column("currency").switch()?; query_builder @@ -102,11 +98,6 @@ where .switch()?; } - query_builder - .add_group_by_clause("attempt_count") - .attach_printable("Error grouping by attempt_count") - .switch()?; - query_builder .add_group_by_clause("currency") .attach_printable("Error grouping by currency") diff --git a/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs b/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs index 07b1bfcf69f..4bb9dc36b07 100644 --- a/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs +++ b/crates/analytics/src/payment_intents/metrics/payments_success_rate.rs @@ -57,10 +57,6 @@ where }) .switch()?; - query_builder - .add_select_column("(attempt_count == 1) as first_attempt".to_string()) - .switch()?; - query_builder .add_select_column(Aggregate::Min { field: "created_at", @@ -90,11 +86,6 @@ where .switch()?; } - query_builder - .add_group_by_clause("first_attempt") - .attach_printable("Error grouping by first_attempt") - .switch()?; - if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs index 2ba75ca8519..0f13ff39af7 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs @@ -59,7 +59,7 @@ where .switch()?; query_builder - .add_select_column("attempt_count == 1 as first_attempt") + .add_select_column("(attempt_count = 1) as first_attempt") .switch()?; query_builder.add_select_column("currency").switch()?; query_builder @@ -98,8 +98,8 @@ where } query_builder - .add_group_by_clause("attempt_count") - .attach_printable("Error grouping by attempt_count") + .add_group_by_clause("first_attempt") + .attach_printable("Error grouping by first_attempt") .switch()?; query_builder .add_group_by_clause("currency") diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs index 0b55c101a7c..437b22aac71 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_distribution.rs @@ -59,7 +59,7 @@ where .switch()?; query_builder - .add_select_column("attempt_count == 1 as first_attempt") + .add_select_column("(attempt_count = 1) as first_attempt") .switch()?; query_builder diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs index 8c340d0b2d6..9716c7ce4de 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payments_success_rate.rs @@ -58,7 +58,7 @@ where .switch()?; query_builder - .add_select_column("(attempt_count == 1) as first_attempt".to_string()) + .add_select_column("(attempt_count = 1) as first_attempt".to_string()) .switch()?; query_builder diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs index b92b7356924..c6d7c59f240 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs @@ -60,7 +60,7 @@ where .switch()?; query_builder - .add_select_column("attempt_count == 1 as first_attempt") + .add_select_column("(attempt_count = 1) as first_attempt") .switch()?; query_builder.add_select_column("currency").switch()?; @@ -105,7 +105,7 @@ where .switch()?; query_builder .add_group_by_clause("currency") - .attach_printable("Error grouping by first_attempt") + .attach_printable("Error grouping by currency") .switch()?; if let Some(granularity) = granularity.as_ref() { granularity diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs index ac08f59f358..f8acb2e6e9a 100644 --- a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -59,9 +59,6 @@ where }) .switch()?; - query_builder - .add_select_column("attempt_count == 1 as first_attempt") - .switch()?; query_builder.add_select_column("currency").switch()?; query_builder .add_select_column(Aggregate::Min { @@ -98,10 +95,6 @@ where .switch()?; } - query_builder - .add_group_by_clause("first_attempt") - .attach_printable("Error grouping by first_attempt") - .switch()?; query_builder .add_group_by_clause("currency") .attach_printable("Error grouping by currency")