From 9460041b2ae8f94f2894517d3c04d30c6f78a5bb Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Sun, 24 Nov 2024 18:32:46 +0530 Subject: [PATCH 01/51] feat(analytics): add `first_attempt` as a filter for PaymentFilters (#6604) --- crates/analytics/src/payments/filters.rs | 1 + crates/analytics/src/payments/types.rs | 5 +++++ crates/analytics/src/query.rs | 6 ++++++ crates/analytics/src/sqlx.rs | 5 +++++ crates/api_models/src/analytics/payments.rs | 2 ++ 5 files changed, 19 insertions(+) diff --git a/crates/analytics/src/payments/filters.rs b/crates/analytics/src/payments/filters.rs index 51805acaae2..668bdaa6c8b 100644 --- a/crates/analytics/src/payments/filters.rs +++ b/crates/analytics/src/payments/filters.rs @@ -64,4 +64,5 @@ pub struct PaymentFilterRow { pub card_last_4: Option, pub card_issuer: Option, pub error_reason: Option, + pub first_attempt: Option, } diff --git a/crates/analytics/src/payments/types.rs b/crates/analytics/src/payments/types.rs index e23fc6cbbb3..b9af3cd0610 100644 --- a/crates/analytics/src/payments/types.rs +++ b/crates/analytics/src/payments/types.rs @@ -104,6 +104,11 @@ where .add_filter_in_range_clause(PaymentDimensions::ErrorReason, &self.error_reason) .attach_printable("Error adding error reason filter")?; } + if !self.first_attempt.is_empty() { + builder + .add_filter_in_range_clause("first_attempt", &self.first_attempt) + .attach_printable("Error adding first attempt filter")?; + } Ok(()) } } diff --git a/crates/analytics/src/query.rs b/crates/analytics/src/query.rs index d746594e36e..e80f762c41b 100644 --- a/crates/analytics/src/query.rs +++ b/crates/analytics/src/query.rs @@ -457,6 +457,12 @@ impl ToSql for common_utils::id_type::CustomerId { } } +impl ToSql for bool { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { + Ok(self.to_string().to_owned()) + } +} + /// Implement `ToSql` on arrays of types that impl `ToString`. macro_rules! impl_to_sql_for_to_string { ($($type:ty),+) => { diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 0a641fbc5f9..16523d5d0a7 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -569,6 +569,10 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::PaymentFilterRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let first_attempt: Option = row.try_get("first_attempt").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; Ok(Self { currency, status, @@ -584,6 +588,7 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::PaymentFilterRow { card_last_4, card_issuer, error_reason, + first_attempt, }) } } diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index 1bade6b5ec8..8fd24f15124 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -41,6 +41,8 @@ pub struct PaymentFilters { pub card_issuer: Vec, #[serde(default)] pub error_reason: Vec, + #[serde(default)] + pub first_attempt: Vec, } #[derive( From 420eaabf3308b2fd2119183b0a2b462aa69b77b2 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:29:08 +0530 Subject: [PATCH 02/51] feat(refunds): Trigger refund outgoing webhooks in create and retrieve refund flows (#6635) Co-authored-by: Chikke Srujan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/core/refunds.rs | 24 ++++++++++- crates/router/src/utils.rs | 72 ++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 86211770e0d..c3e49a49bb1 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -363,6 +363,16 @@ pub async fn trigger_refund_to_gateway( refund.refund_id ) })?; + utils::trigger_refund_outgoing_webhook( + state, + merchant_account, + &response, + payment_attempt.profile_id.clone(), + key_store, + ) + .await + .map_err(|error| logger::warn!(refunds_outgoing_webhook_error=?error)) + .ok(); Ok(response) } @@ -467,7 +477,7 @@ pub async fn refund_retrieve_core( .transpose()?; let response = if should_call_refund(&refund, request.force_sync.unwrap_or(false)) { - sync_refund_with_gateway( + Box::pin(sync_refund_with_gateway( &state, &merchant_account, &key_store, @@ -476,7 +486,7 @@ pub async fn refund_retrieve_core( &refund, creds_identifier, charges_req, - ) + )) .await } else { Ok(refund) @@ -669,6 +679,16 @@ pub async fn sync_refund_with_gateway( refund.refund_id ) })?; + utils::trigger_refund_outgoing_webhook( + state, + merchant_account, + &response, + payment_attempt.profile_id.clone(), + key_store, + ) + .await + .map_err(|error| logger::warn!(refunds_outgoing_webhook_error=?error)) + .ok(); Ok(response) } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index ca61543ec57..9dca6cf3477 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -59,7 +59,10 @@ use crate::{ logger, routes::{metrics, SessionState}, services, - types::{self, domain, transformers::ForeignFrom}, + types::{ + self, domain, + transformers::{ForeignFrom, ForeignInto}, + }, }; pub mod error_parser { @@ -1274,3 +1277,70 @@ pub async fn flatten_join_error(handle: Handle) -> RouterResult { .attach_printable("Join Error"), } } + +#[cfg(feature = "v1")] +pub async fn trigger_refund_outgoing_webhook( + state: &SessionState, + merchant_account: &domain::MerchantAccount, + refund: &diesel_models::Refund, + profile_id: id_type::ProfileId, + key_store: &domain::MerchantKeyStore, +) -> RouterResult<()> { + let refund_status = refund.refund_status; + if matches!( + refund_status, + enums::RefundStatus::Success + | enums::RefundStatus::Failure + | enums::RefundStatus::TransactionFailure + ) { + let event_type = ForeignFrom::foreign_from(refund_status); + let refund_response: api_models::refunds::RefundResponse = refund.clone().foreign_into(); + let key_manager_state = &(state).into(); + let refund_id = refund_response.refund_id.clone(); + let business_profile = state + .store + .find_business_profile_by_profile_id(key_manager_state, key_store, &profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id.get_string_repr().to_owned(), + })?; + let cloned_state = state.clone(); + let cloned_key_store = key_store.clone(); + let cloned_merchant_account = merchant_account.clone(); + let primary_object_created_at = refund_response.created_at; + if let Some(outgoing_event_type) = event_type { + tokio::spawn( + async move { + Box::pin(webhooks_core::create_event_and_trigger_outgoing_webhook( + cloned_state, + cloned_merchant_account, + business_profile, + &cloned_key_store, + outgoing_event_type, + diesel_models::enums::EventClass::Refunds, + refund_id.to_string(), + diesel_models::enums::EventObjectType::RefundDetails, + webhooks::OutgoingWebhookContent::RefundDetails(Box::new(refund_response)), + primary_object_created_at, + )) + .await + } + .in_current_span(), + ); + } else { + logger::warn!("Outgoing webhook not sent because of missing event type status mapping"); + }; + } + Ok(()) +} + +#[cfg(feature = "v2")] +pub async fn trigger_refund_outgoing_webhook( + state: &SessionState, + merchant_account: &domain::MerchantAccount, + refund: &diesel_models::Refund, + profile_id: id_type::ProfileId, + key_store: &domain::MerchantKeyStore, +) -> RouterResult<()> { + todo!() +} From 62dc4e6015f4a8310aee9de05f37759ee0e9221a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:22:18 +0000 Subject: [PATCH 03/51] chore(version): 2024.11.25.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eee4bf49472..2982af5e0db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.25.0 + +### Features + +- **analytics:** Add `first_attempt` as a filter for PaymentFilters ([#6604](https://github.com/juspay/hyperswitch/pull/6604)) ([`9460041`](https://github.com/juspay/hyperswitch/commit/9460041b2ae8f94f2894517d3c04d30c6f78a5bb)) +- **refunds:** Trigger refund outgoing webhooks in create and retrieve refund flows ([#6635](https://github.com/juspay/hyperswitch/pull/6635)) ([`420eaab`](https://github.com/juspay/hyperswitch/commit/420eaabf3308b2fd2119183b0a2b462aa69b77b2)) + +### Bug Fixes + +- **analytics:** Remove first_attempt group by in Payment Intent old metrics ([#6627](https://github.com/juspay/hyperswitch/pull/6627)) ([`54e393b`](https://github.com/juspay/hyperswitch/commit/54e393bf9a55bdc4527a723b7a03968f21848a5e)) +- **connector:** [Cybersource] change commerce indicator for applepay ([#6634](https://github.com/juspay/hyperswitch/pull/6634)) ([`8d0639e`](https://github.com/juspay/hyperswitch/commit/8d0639ea6f22227253a44e6bd8272d9e55d17f92)) + +**Full Changelog:** [`2024.11.22.0...2024.11.25.0`](https://github.com/juspay/hyperswitch/compare/2024.11.22.0...2024.11.25.0) + +- - - + ## 2024.11.22.0 ### Features From 83e8bc0775c20e9d055e65bd13a2e8b1148092e1 Mon Sep 17 00:00:00 2001 From: Kiran Kumar <60121719+KiranKBR@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:29:51 +0530 Subject: [PATCH 04/51] feat(connector): [Paypal] implement vaulting for paypal cards via zero mandates (#5324) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: swangi-kumari --- crates/router/src/connector/paypal.rs | 81 ++++++++-- .../src/connector/paypal/transformers.rs | 141 +++++++++++++++++- .../cypress/e2e/PaymentUtils/Paypal.js | 69 +++++++-- 3 files changed, 260 insertions(+), 31 deletions(-) diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index e79912f61ff..04a5ae6f756 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -652,19 +652,78 @@ impl types::PaymentsResponseData, > for Paypal { - fn build_request( + fn get_headers( &self, - _req: &types::RouterData< - api::SetupMandate, - types::SetupMandateRequestData, - types::PaymentsResponseData, - >, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + fn get_url( + &self, + _req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}v3/vault/payment-tokens/", + self.base_url(connectors) + )) + } + fn get_request_body( + &self, + req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - Err( - errors::ConnectorError::NotImplemented("Setup Mandate flow for Paypal".to_string()) - .into(), - ) + ) -> CustomResult { + let connector_req = paypal::PaypalZeroMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .set_body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::SetupMandateRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalSetupMandatesResponse = res + .response + .parse_struct("PaypalSetupMandatesResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::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) } } diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index ed8369607df..7f2d0d0e608 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -425,7 +425,7 @@ pub struct RedirectRequest { experience_context: ContextStruct, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ContextStruct { return_url: Option, cancel_url: Option, @@ -433,13 +433,13 @@ pub struct ContextStruct { shipping_preference: ShippingPreference, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum UserAction { #[serde(rename = "PAY_NOW")] PayNow, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum ShippingPreference { #[serde(rename = "SET_PROVIDED_ADDRESS")] SetProvidedAddress, @@ -527,6 +527,132 @@ pub struct PaypalPaymentsRequest { payment_source: Option, } +#[derive(Debug, Serialize)] +pub struct PaypalZeroMandateRequest { + payment_source: ZeroMandateSourceItem, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ZeroMandateSourceItem { + Card(CardMandateRequest), + Paypal(PaypalMandateStruct), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaypalMandateStruct { + experience_context: Option, + usage_type: UsageType, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CardMandateRequest { + billing_address: Option
, + expiry: Option>, + name: Option>, + number: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PaypalSetupMandatesResponse { + id: String, + customer: Customer, + payment_source: ZeroMandateSourceItem, + links: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Customer { + id: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PaypalSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let info_response = item.response; + + let mandate_reference = Some(MandateReference { + connector_mandate_id: Some(info_response.id.clone()), + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + }); + // https://developer.paypal.com/docs/api/payment-tokens/v3/#payment-tokens_create + // If 201 status code, then order is captured, other status codes are handled by the error handler + let status = if item.http_code == 201 { + enums::AttemptStatus::Charged + } else { + enums::AttemptStatus::Failure + }; + Ok(Self { + status, + return_url: None, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(mandate_reference), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(info_response.id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} +impl TryFrom<&types::SetupMandateRouterData> for PaypalZeroMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::SetupMandateRouterData) -> Result { + let payment_source = match item.request.payment_method_data.clone() { + domain::PaymentMethodData::Card(ccard) => { + ZeroMandateSourceItem::Card(CardMandateRequest { + billing_address: get_address_info(item.get_optional_billing()), + expiry: Some(ccard.get_expiry_date_as_yyyymm("-")), + name: item.get_optional_billing_full_name(), + number: Some(ccard.card_number), + }) + } + + domain::PaymentMethodData::Wallet(_) + | domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankDebit(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::MandatePayment + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::RealTimePayment(_) + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::CardToken(_) + | domain::PaymentMethodData::CardDetailsForNetworkTransactionId(_) + | domain::PaymentMethodData::NetworkToken(_) + | domain::PaymentMethodData::OpenBanking(_) + | domain::PaymentMethodData::MobilePayment(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ))? + } + }; + + Ok(Self { payment_source }) + } +} + fn get_address_info(payment_address: Option<&api_models::payments::Address>) -> Option
{ let address = payment_address.and_then(|payment_address| payment_address.address.as_ref()); match address { @@ -973,11 +1099,11 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP )?; let payment_source = match payment_method_type { - enums::PaymentMethodType::Credit => Ok(Some(PaymentSourceItem::Card( - CardRequest::CardVaultStruct(VaultStruct { + enums::PaymentMethodType::Credit | enums::PaymentMethodType::Debit => Ok(Some( + PaymentSourceItem::Card(CardRequest::CardVaultStruct(VaultStruct { vault_id: connector_mandate_id.into(), - }), - ))), + })), + )), enums::PaymentMethodType::Paypal => Ok(Some(PaymentSourceItem::Paypal( PaypalRedirectionRequest::PaypalVaultStruct(VaultStruct { vault_id: connector_mandate_id.into(), @@ -1009,7 +1135,6 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | enums::PaymentMethodType::Cashapp | enums::PaymentMethodType::Dana | enums::PaymentMethodType::DanamonVa - | enums::PaymentMethodType::Debit | enums::PaymentMethodType::DirectCarrierBilling | enums::PaymentMethodType::DuitNow | enums::PaymentMethodType::Efecty diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js b/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js index 269fd3ea0b8..4e7f49b1375 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js @@ -16,6 +16,23 @@ const successfulThreeDSTestCardDetails = { card_cvc: "123", }; +const singleUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + single_use: { + amount: 8000, + currency: "USD", + }, + }, +}; + export const connectorDetails = { card_pm: { PaymentIntent: { @@ -222,14 +239,18 @@ export const connectorDetails = { }, }, ZeroAuthMandate: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, Response: { - status: 501, + status: 200, body: { - error: { - type: "invalid_request", - message: "Setup Mandate flow for Paypal is not implemented", - code: "IR_00", - }, + status: "succeeded", }, }, }, @@ -257,13 +278,37 @@ export const connectorDetails = { }, }, Response: { - status: 501, + status: 200, body: { - error: { - type: "invalid_request", - message: "Setup Mandate flow for Paypal is not implemented", - code: "IR_00", - }, + status: "succeeded", + setup_future_usage: "off_session", + }, + }, + }, + SaveCardConfirmAutoCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentIntentOffSession: { + Request: { + currency: "USD", + amount: 6500, + authentication_type: "no_three_ds", + customer_acceptance: null, + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", }, }, }, From 2b8eb09a16040957ac369c48e6095c343207f0d3 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:24:19 +0530 Subject: [PATCH 05/51] feat(core): add SCA exemption field (#6578) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 8 ++++ api-reference/openapi_spec.json | 40 +++++++++++++++++++ crates/api_models/src/payments.rs | 4 ++ crates/common_enums/src/enums.rs | 25 +++++++++++- crates/diesel_models/src/enums.rs | 6 +-- crates/diesel_models/src/payment_intent.rs | 3 ++ crates/diesel_models/src/schema.rs | 1 + crates/diesel_models/src/schema_v2.rs | 1 + .../hyperswitch_domain_models/src/payments.rs | 1 + .../src/payments/payment_intent.rs | 4 ++ .../src/router_data.rs | 3 ++ crates/openapi/src/openapi.rs | 1 + crates/openapi/src/openapi_v2.rs | 1 + crates/router/src/core/authentication.rs | 2 + .../src/core/authentication/transformers.rs | 6 +++ .../core/fraud_check/flows/checkout_flow.rs | 1 + .../fraud_check/flows/fulfillment_flow.rs | 1 + .../core/fraud_check/flows/record_return.rs | 1 + .../src/core/fraud_check/flows/sale_flow.rs | 1 + .../fraud_check/flows/transaction_flow.rs | 1 + crates/router/src/core/mandate/utils.rs | 1 + crates/router/src/core/payments.rs | 1 + crates/router/src/core/payments/helpers.rs | 4 ++ .../payments/operations/payment_confirm.rs | 4 ++ .../payments/operations/payment_create.rs | 1 + .../payments/operations/payment_update.rs | 3 ++ .../router/src/core/payments/transformers.rs | 4 ++ crates/router/src/core/utils.rs | 8 ++++ crates/router/src/core/webhooks/utils.rs | 1 + .../router/src/services/conversion_impls.rs | 1 + crates/router/src/types.rs | 2 + .../router/src/types/api/verify_connector.rs | 1 + crates/router/src/utils/user/sample_data.rs | 1 + crates/router/tests/connectors/aci.rs | 2 + crates/router/tests/connectors/utils.rs | 1 + .../down.sql | 4 ++ .../up.sql | 6 +++ 37 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/down.sql create mode 100644 migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/up.sql diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 095e386d981..2a5b1cfa310 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -18645,6 +18645,14 @@ } } }, + "ScaExemptionType": { + "type": "string", + "description": "SCA Exemptions types available for authentication", + "enum": [ + "low_value", + "transaction_risk_analysis" + ] + }, "SdkInformation": { "type": "object", "description": "SDK Information if request is from SDK", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 247b308bd65..e029bef9453 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -17019,6 +17019,14 @@ "type": "boolean", "description": "Whether to calculate tax for this payment intent", "nullable": true + }, + "psd2_sca_exemption_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ScaExemptionType" + } + ], + "nullable": true } } }, @@ -17389,6 +17397,14 @@ "type": "boolean", "description": "Whether to calculate tax for this payment intent", "nullable": true + }, + "psd2_sca_exemption_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ScaExemptionType" + } + ], + "nullable": true } } }, @@ -18583,6 +18599,14 @@ "type": "boolean", "description": "Whether to calculate tax for this payment intent", "nullable": true + }, + "psd2_sca_exemption_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ScaExemptionType" + } + ], + "nullable": true } }, "additionalProperties": false @@ -19616,6 +19640,14 @@ "type": "boolean", "description": "Whether to calculate tax for this payment intent", "nullable": true + }, + "psd2_sca_exemption_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ScaExemptionType" + } + ], + "nullable": true } } }, @@ -23284,6 +23316,14 @@ } } }, + "ScaExemptionType": { + "type": "string", + "description": "SCA Exemptions types available for authentication", + "enum": [ + "low_value", + "transaction_risk_analysis" + ] + }, "SdkInformation": { "type": "object", "description": "SDK Information if request is from SDK", diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 98bc7b754a4..06dd7dc7969 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -859,6 +859,10 @@ pub struct PaymentsRequest { /// Whether to calculate tax for this payment intent pub skip_external_tax_calculation: Option, + + /// Choose what kind of sca exemption is required for this payment + #[schema(value_type = Option)] + pub psd2_sca_exemption_type: Option, } #[cfg(feature = "v1")] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 23dbab77825..781b5e3710a 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -20,7 +20,7 @@ pub mod diesel_exports { DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, - DbWebhookDeliveryAttempt as WebhookDeliveryAttempt, + DbScaExemptionType as ScaExemptionType, DbWebhookDeliveryAttempt as WebhookDeliveryAttempt, }; } @@ -1668,6 +1668,29 @@ pub enum PaymentType { RecurringMandate, } +/// SCA Exemptions types available for authentication +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ScaExemptionType { + #[default] + LowValue, + TransactionRiskAnalysis, +} + #[derive( Clone, Copy, diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 77d167402ef..5de048e32ef 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -20,9 +20,9 @@ pub mod diesel_exports { DbRefundStatus as RefundStatus, DbRefundType as RefundType, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind, - DbTotpStatus as TotpStatus, DbTransactionType as TransactionType, - DbUserRoleVersion as UserRoleVersion, DbUserStatus as UserStatus, - DbWebhookDeliveryAttempt as WebhookDeliveryAttempt, + DbScaExemptionType as ScaExemptionType, DbTotpStatus as TotpStatus, + DbTransactionType as TransactionType, DbUserRoleVersion as UserRoleVersion, + DbUserStatus as UserStatus, DbWebhookDeliveryAttempt as WebhookDeliveryAttempt, }; } pub use common_enums::*; diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 26cb0b8c8a8..7826e2dadd2 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -72,6 +72,7 @@ pub struct PaymentIntent { pub routing_algorithm_id: Option, pub payment_link_config: Option, pub id: common_utils::id_type::GlobalPaymentId, + pub psd2_sca_exemption_type: Option, } #[cfg(feature = "v1")] @@ -136,6 +137,7 @@ pub struct PaymentIntent { pub organization_id: common_utils::id_type::OrganizationId, pub tax_details: Option, pub skip_external_tax_calculation: Option, + pub psd2_sca_exemption_type: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq)] @@ -352,6 +354,7 @@ pub struct PaymentIntentNew { pub organization_id: common_utils::id_type::OrganizationId, pub tax_details: Option, pub skip_external_tax_calculation: Option, + pub psd2_sca_exemption_type: Option, } #[cfg(feature = "v2")] diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 6dddbb754d5..d3e560fc048 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -932,6 +932,7 @@ diesel::table! { organization_id -> Varchar, tax_details -> Nullable, skip_external_tax_calculation -> Nullable, + psd2_sca_exemption_type -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index d44dd3317d3..f6bab9071cd 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -895,6 +895,7 @@ diesel::table! { payment_link_config -> Nullable, #[max_length = 64] id -> Varchar, + psd2_sca_exemption_type -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 1bab8ae3b76..276035214a4 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -99,6 +99,7 @@ pub struct PaymentIntent { pub organization_id: id_type::OrganizationId, pub tax_details: Option, pub skip_external_tax_calculation: Option, + pub psd2_sca_exemption_type: Option, } impl PaymentIntent { diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 4f2053ec6f9..d10dafe883a 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1283,6 +1283,7 @@ impl behaviour::Conversion for PaymentIntent { customer_present: Some(customer_present.as_bool()), payment_link_config, routing_algorithm_id, + psd2_sca_exemption_type: None, }) } async fn convert_back( @@ -1551,6 +1552,7 @@ impl behaviour::Conversion for PaymentIntent { shipping_cost: self.shipping_cost, tax_details: self.tax_details, skip_external_tax_calculation: self.skip_external_tax_calculation, + psd2_sca_exemption_type: self.psd2_sca_exemption_type, }) } @@ -1638,6 +1640,7 @@ impl behaviour::Conversion for PaymentIntent { is_payment_processor_token_flow: storage_model.is_payment_processor_token_flow, organization_id: storage_model.organization_id, skip_external_tax_calculation: storage_model.skip_external_tax_calculation, + psd2_sca_exemption_type: storage_model.psd2_sca_exemption_type, }) } .await @@ -1700,6 +1703,7 @@ impl behaviour::Conversion for PaymentIntent { shipping_cost: self.shipping_cost, tax_details: self.tax_details, skip_external_tax_calculation: self.skip_external_tax_calculation, + psd2_sca_exemption_type: self.psd2_sca_exemption_type, }) } } diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 0863ab59b45..a3867ce3e62 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -86,6 +86,9 @@ pub struct RouterData { pub header_payload: Option, pub connector_mandate_request_reference_id: Option, + + /// Contains the type of sca exemption required for the transaction + pub psd2_sca_exemption_type: Option, } // Different patterns of authentication. diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 6356b5f15be..ed6efea27f8 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -254,6 +254,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::AcceptedCountries, api_models::admin::AcceptedCurrencies, api_models::enums::PaymentType, + api_models::enums::ScaExemptionType, api_models::enums::PaymentMethod, api_models::enums::PaymentMethodType, api_models::enums::ConnectorType, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 17c00416933..4198e90882e 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -199,6 +199,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::AcceptedCurrencies, api_models::enums::ProductType, api_models::enums::PaymentType, + api_models::enums::ScaExemptionType, api_models::enums::PaymentMethod, api_models::enums::PaymentMethodType, api_models::enums::ConnectorType, diff --git a/crates/router/src/core/authentication.rs b/crates/router/src/core/authentication.rs index 29508b5c0f5..b4718a64de5 100644 --- a/crates/router/src/core/authentication.rs +++ b/crates/router/src/core/authentication.rs @@ -39,6 +39,7 @@ pub async fn perform_authentication( email: Option, webhook_url: String, three_ds_requestor_url: String, + psd2_sca_exemption_type: Option, ) -> CustomResult { let router_data = transformers::construct_authentication_router_data( merchant_id, @@ -60,6 +61,7 @@ pub async fn perform_authentication( email, webhook_url, three_ds_requestor_url, + psd2_sca_exemption_type, )?; let response = Box::pin(utils::do_auth_connector_call( state, diff --git a/crates/router/src/core/authentication/transformers.rs b/crates/router/src/core/authentication/transformers.rs index 6b8a378a070..bcbd1481711 100644 --- a/crates/router/src/core/authentication/transformers.rs +++ b/crates/router/src/core/authentication/transformers.rs @@ -43,6 +43,7 @@ pub fn construct_authentication_router_data( email: Option, webhook_url: String, three_ds_requestor_url: String, + psd2_sca_exemption_type: Option, ) -> RouterResult { let router_request = types::authentication::ConnectorAuthenticationRequestData { payment_method_data, @@ -70,6 +71,7 @@ pub fn construct_authentication_router_data( types::PaymentAddress::default(), router_request, &merchant_connector_account, + psd2_sca_exemption_type, ) } @@ -94,6 +96,7 @@ pub fn construct_post_authentication_router_data( types::PaymentAddress::default(), router_request, &merchant_connector_account, + None, ) } @@ -119,6 +122,7 @@ pub fn construct_pre_authentication_router_data( types::PaymentAddress::default(), router_request, merchant_connector_account, + None, ) } @@ -129,6 +133,7 @@ pub fn construct_router_data( address: types::PaymentAddress, request_data: Req, merchant_connector_account: &payments_helpers::MerchantConnectorAccountType, + psd2_sca_exemption_type: Option, ) -> RouterResult> { let test_mode: Option = merchant_connector_account.is_test_mode_on(); let auth_type: types::ConnectorAuthType = merchant_connector_account @@ -185,6 +190,7 @@ pub fn construct_router_data( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type, }) } diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index af59c6c4215..84ba1f26a5d 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -161,6 +161,7 @@ impl ConstructFlowSpecificData( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index 6bf5688104b..6e29d01c81f 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -129,6 +129,7 @@ impl ConstructFlowSpecificData( additional_merchant_data: router_data.additional_merchant_data, header_payload: router_data.header_payload, connector_mandate_request_reference_id: router_data.connector_mandate_request_reference_id, + psd2_sca_exemption_type: router_data.psd2_sca_exemption_type, } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 8c82c38be71..b56260b407e 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -353,6 +353,10 @@ impl GetTracker, api::PaymentsRequest> for Pa .setup_future_usage .or(payment_intent.setup_future_usage); + payment_intent.psd2_sca_exemption_type = request + .psd2_sca_exemption_type + .or(payment_intent.psd2_sca_exemption_type); + let browser_info = request .browser_info .clone() diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 1f0884fe384..dcc2ba6a289 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1479,6 +1479,7 @@ impl PaymentCreate { shipping_cost: request.shipping_cost, tax_details: None, skip_external_tax_calculation, + psd2_sca_exemption_type: request.psd2_sca_exemption_type, }) } diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 2f1a0c333fa..98491cab1db 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -271,6 +271,9 @@ impl GetTracker, api::PaymentsRequest> for Pa .or(payment_intent.feature_metadata); payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); payment_intent.frm_metadata = request.frm_metadata.clone().or(payment_intent.frm_metadata); + payment_intent.psd2_sca_exemption_type = request + .psd2_sca_exemption_type + .or(payment_intent.psd2_sca_exemption_type); Self::populate_payment_intent_with_request(&mut payment_intent, request); let token = token.or_else(|| payment_attempt.payment_token.clone()); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8a0983eccab..d9bd374ef1a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -168,6 +168,7 @@ where additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id, + psd2_sca_exemption_type: None, }; Ok(router_data) } @@ -370,6 +371,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( additional_merchant_data: None, header_payload, connector_mandate_request_reference_id, + psd2_sca_exemption_type: None, }; Ok(router_data) @@ -502,6 +504,7 @@ pub async fn construct_router_data_for_psync<'a>( additional_merchant_data: None, header_payload, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) @@ -723,6 +726,7 @@ where }), header_payload, connector_mandate_request_reference_id, + psd2_sca_exemption_type: payment_data.payment_intent.psd2_sca_exemption_type, }; Ok(router_data) diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index fea112bc3b8..167a4c59006 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -215,6 +215,7 @@ pub async fn construct_payout_router_data<'a, F>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) @@ -394,6 +395,7 @@ pub async fn construct_refund_router_data<'a, F>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) @@ -705,6 +707,7 @@ pub async fn construct_accept_dispute_router_data<'a>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } @@ -801,6 +804,7 @@ pub async fn construct_submit_evidence_router_data<'a>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } @@ -903,6 +907,7 @@ pub async fn construct_upload_file_router_data<'a>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } @@ -1025,6 +1030,7 @@ pub async fn construct_payments_dynamic_tax_calculation_router_data<'a, F: Clone additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } @@ -1124,6 +1130,7 @@ pub async fn construct_defend_dispute_router_data<'a>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } @@ -1217,6 +1224,7 @@ pub async fn construct_retrieve_file_router_data<'a>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index fff675503ad..f1e84b2226a 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -123,6 +123,7 @@ pub async fn construct_webhook_router_data<'a>( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, }; Ok(router_data) } diff --git a/crates/router/src/services/conversion_impls.rs b/crates/router/src/services/conversion_impls.rs index 33c655eed78..6ac934cd2fd 100644 --- a/crates/router/src/services/conversion_impls.rs +++ b/crates/router/src/services/conversion_impls.rs @@ -77,6 +77,7 @@ fn get_default_router_data( additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index bea009949f9..467ae31f7cf 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -948,6 +948,7 @@ impl ForeignFrom<(&RouterData, T2) connector_mandate_request_reference_id: data .connector_mandate_request_reference_id .clone(), + psd2_sca_exemption_type: data.psd2_sca_exemption_type, } } } @@ -1013,6 +1014,7 @@ impl additional_merchant_data: data.additional_merchant_data.clone(), header_payload: data.header_payload.clone(), connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index a472296d2a7..c368a3fb37d 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -118,6 +118,7 @@ impl VerifyConnectorData { additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, } } } diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 8ffb0f2001f..600d610e428 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -274,6 +274,7 @@ pub async fn generate_sample_data( shipping_cost: None, tax_details: None, skip_external_tax_calculation: None, + psd2_sca_exemption_type: None, }; let (connector_transaction_id, connector_transaction_data) = ConnectorTransactionId::form_id_and_data(attempt_id.clone()); diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 6f4855d1e22..10c8a3dd012 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -129,6 +129,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, } } @@ -199,6 +200,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, } } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 52218b211ac..3402b532fbf 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -547,6 +547,7 @@ pub trait ConnectorActions: Connector { additional_merchant_data: None, header_payload: None, connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, } } diff --git a/migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/down.sql b/migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/down.sql new file mode 100644 index 00000000000..f43aff7f567 --- /dev/null +++ b/migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN psd2_sca_exemption_type; + +DROP TYPE "ScaExemptionType"; \ No newline at end of file diff --git a/migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/up.sql b/migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/up.sql new file mode 100644 index 00000000000..6fa361a4802 --- /dev/null +++ b/migrations/2024-11-14-084429_add_sca_exemption_field_to_payment_intent/up.sql @@ -0,0 +1,6 @@ +CREATE TYPE "ScaExemptionType" AS ENUM ( + 'low_value', + 'transaction_risk_analysis' +); + +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS psd2_sca_exemption_type "ScaExemptionType"; \ No newline at end of file From 0db3aed1533856b9892369d7bb2430d90d091756 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:58:13 +0530 Subject: [PATCH 06/51] chore(deps): update cypress packages to address CVE (#6624) --- cypress-tests-v2/package-lock.json | 14 +-- cypress-tests-v2/package.json | 2 +- cypress-tests/package-lock.json | 186 ++++++++++++----------------- cypress-tests/package.json | 6 +- 4 files changed, 84 insertions(+), 124 deletions(-) diff --git a/cypress-tests-v2/package-lock.json b/cypress-tests-v2/package-lock.json index 36f801468a9..cac88cab055 100644 --- a/cypress-tests-v2/package-lock.json +++ b/cypress-tests-v2/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "devDependencies": { "@types/fs-extra": "^11.0.4", - "cypress": "^13.15.2", + "cypress": "^13.16.0", "cypress-mochawesome-reporter": "^3.8.2", "jsqr": "^1.4.0", "nanoid": "^5.0.8", @@ -727,8 +727,8 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "license": "MIT", @@ -742,9 +742,9 @@ } }, "node_modules/cypress": { - "version": "13.15.2", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.2.tgz", - "integrity": "sha512-ARbnUorjcCM3XiPwgHKuqsyr5W9Qn+pIIBPaoilnoBkLdSC2oLQjV1BUpnmc7KR+b7Avah3Ly2RMFnfxr96E/A==", + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.0.tgz", + "integrity": "sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1057,7 +1057,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", diff --git a/cypress-tests-v2/package.json b/cypress-tests-v2/package.json index 20403f9e777..37a51db93cb 100644 --- a/cypress-tests-v2/package.json +++ b/cypress-tests-v2/package.json @@ -15,7 +15,7 @@ "license": "ISC", "devDependencies": { "@types/fs-extra": "^11.0.4", - "cypress": "^13.15.2", + "cypress": "^13.16.0", "cypress-mochawesome-reporter": "^3.8.2", "jsqr": "^1.4.0", "prettier": "^3.3.2", diff --git a/cypress-tests/package-lock.json b/cypress-tests/package-lock.json index abac33d9e32..04390f76f1d 100644 --- a/cypress-tests/package-lock.json +++ b/cypress-tests/package-lock.json @@ -8,13 +8,11 @@ "name": "test", "version": "1.0.0", "license": "ISC", - "dependencies": { - "prettier": "^3.3.2" - }, "devDependencies": { - "cypress": "^13.14.1", + "cypress": "^13.16.0", "cypress-mochawesome-reporter": "^3.8.2", - "jsqr": "^1.4.0" + "jsqr": "^1.4.0", + "prettier": "^3.3.2" } }, "node_modules/@colors/colors": { @@ -29,9 +27,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.6.tgz", + "integrity": "sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -41,16 +39,16 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.0", "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", + "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -290,9 +288,9 @@ } }, "node_modules/aws4": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.1.tgz", - "integrity": "sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "dev": true, "license": "MIT" }, @@ -548,9 +546,9 @@ } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "dev": true, "funding": [ { @@ -707,9 +705,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -722,14 +720,14 @@ } }, "node_modules/cypress": { - "version": "13.14.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.1.tgz", - "integrity": "sha512-Wo+byPmjps66hACEH5udhXINEiN3qS3jWNGRzJOjrRJF3D0+YrcP2LVB1T7oYaVQM/S+eanqEvBWYc8cf7Vcbg==", + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.0.tgz", + "integrity": "sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.1", + "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -740,6 +738,7 @@ "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", "commander": "^6.2.1", @@ -754,7 +753,6 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -769,6 +767,7 @@ "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.3", + "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, @@ -1036,7 +1035,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", @@ -1184,18 +1183,18 @@ } }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/fs-extra": { @@ -1466,15 +1465,15 @@ } }, "node_modules/http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "dev": true, "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" }, "engines": { "node": ">=0.10" @@ -1564,19 +1563,6 @@ "node": ">=8" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2481,9 +2467,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "license": "MIT", "engines": { @@ -2668,6 +2654,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -2721,13 +2708,6 @@ "dev": true, "license": "MIT" }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -2739,24 +2719,14 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -2765,13 +2735,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2831,13 +2794,6 @@ "dev": true, "license": "ISC" }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -3138,6 +3094,26 @@ "dev": true, "license": "MIT" }, + "node_modules/tldts": { + "version": "6.1.62", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.62.tgz", + "integrity": "sha512-TF+wo3MgTLbf37keEwQD0IxvOZO8UZxnpPJDg5iFGAASGxYzbX/Q0y944ATEjrfxG/pF1TWRHCPbFp49Mz1Y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.62" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.62", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.62.tgz", + "integrity": "sha512-ohONqbfobpuaylhqFbtCzc0dFFeNz85FVKSesgT8DS9OV3a25Yj730pTj7/dDtCqmgoCgEj6gDiU9XxgHKQlBw==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -3163,29 +3139,26 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" + "node": ">=16" } }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "bin": { + "tree-kill": "cli.js" } }, "node_modules/tslib": { @@ -3256,17 +3229,6 @@ "node": ">=8" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/cypress-tests/package.json b/cypress-tests/package.json index 7bb40354efd..204e3393d0a 100644 --- a/cypress-tests/package.json +++ b/cypress-tests/package.json @@ -15,11 +15,9 @@ "author": "", "license": "ISC", "devDependencies": { - "cypress": "^13.14.1", + "cypress": "^13.16.0", "cypress-mochawesome-reporter": "^3.8.2", - "jsqr": "^1.4.0" - }, - "dependencies": { + "jsqr": "^1.4.0", "prettier": "^3.3.2" } } From 57e64c26ca4251b493c87bfe93799faaab4ffa89 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:04:53 +0530 Subject: [PATCH 07/51] feat(payments): add merchant order ref id filter (#6630) --- crates/api_models/src/payments.rs | 2 ++ .../src/payments/payment_intent.rs | 5 +++++ crates/router/src/types/storage/dispute.rs | 10 ++++++---- crates/router/src/types/storage/refund.rs | 17 +++++++++++------ .../storage_impl/src/payments/payment_intent.rs | 11 +++++++++++ 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 06dd7dc7969..c0cf816c68d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4927,6 +4927,8 @@ pub struct PaymentListFilterConstraints { pub order: Order, /// The List of all the card networks to filter payments list pub card_network: Option>, + /// The identifier for merchant order reference id + pub merchant_order_reference_id: Option, } impl PaymentListFilterConstraints { diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index d10dafe883a..b0ffe519d63 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1002,6 +1002,7 @@ pub struct PaymentIntentListParams { pub limit: Option, pub order: api_models::payments::Order, pub card_network: Option>, + pub merchant_order_reference_id: Option, } impl From for PaymentIntentFetchConstraints { @@ -1036,6 +1037,7 @@ impl From for PaymentIntentFetchCo limit: Some(std::cmp::min(limit, PAYMENTS_LIST_MAX_LIMIT_V1)), order: Default::default(), card_network: None, + merchant_order_reference_id: None, })) } } @@ -1061,6 +1063,7 @@ impl From for PaymentIntentFetchConstraints { limit: None, order: Default::default(), card_network: None, + merchant_order_reference_id: None, })) } } @@ -1084,6 +1087,7 @@ impl From for PaymentIntentF merchant_connector_id, order, card_network, + merchant_order_reference_id, } = value; if let Some(payment_intent_id) = payment_id { Self::Single { payment_intent_id } @@ -1107,6 +1111,7 @@ impl From for PaymentIntentF limit: Some(std::cmp::min(limit, PAYMENTS_LIST_MAX_LIMIT_V2)), order, card_network, + merchant_order_reference_id, })) } } diff --git a/crates/router/src/types/storage/dispute.rs b/crates/router/src/types/storage/dispute.rs index 2d42aaa0a8a..cf1ff52960f 100644 --- a/crates/router/src/types/storage/dispute.rs +++ b/crates/router/src/types/storage/dispute.rs @@ -1,6 +1,6 @@ use async_bb8_diesel::AsyncRunQueryDsl; use common_utils::errors::CustomResult; -use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, QueryDsl}; pub use diesel_models::dispute::{Dispute, DisputeNew, DisputeUpdate}; use diesel_models::{errors, query::generics::db_metrics, schema::dispute::dsl}; use error_stack::ResultExt; @@ -43,9 +43,11 @@ impl DisputeDbExt for Dispute { &dispute_list_constraints.dispute_id, ) { search_by_payment_or_dispute_id = true; - filter = filter - .filter(dsl::payment_id.eq(payment_id.to_owned())) - .or_filter(dsl::dispute_id.eq(dispute_id.to_owned())); + filter = filter.filter( + dsl::payment_id + .eq(payment_id.to_owned()) + .or(dsl::dispute_id.eq(dispute_id.to_owned())), + ); }; if !search_by_payment_or_dispute_id { diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index 8076c2c7a6e..e46fc1a3f47 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -1,7 +1,7 @@ use api_models::payments::AmountFilter; use async_bb8_diesel::AsyncRunQueryDsl; use common_utils::errors::CustomResult; -use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, QueryDsl}; pub use diesel_models::refund::{ Refund, RefundCoreWorkflow, RefundNew, RefundUpdate, RefundUpdateInternal, }; @@ -67,8 +67,11 @@ impl RefundDbExt for Refund { ) { search_by_pay_or_ref_id = true; filter = filter - .filter(dsl::payment_id.eq(pid.to_owned())) - .or_filter(dsl::refund_id.eq(ref_id.to_owned())) + .filter( + dsl::payment_id + .eq(pid.to_owned()) + .or(dsl::refund_id.eq(ref_id.to_owned())), + ) .limit(limit) .offset(offset); }; @@ -228,9 +231,11 @@ impl RefundDbExt for Refund { &refund_list_details.refund_id, ) { search_by_pay_or_ref_id = true; - filter = filter - .filter(dsl::payment_id.eq(pid.to_owned())) - .or_filter(dsl::refund_id.eq(ref_id.to_owned())); + filter = filter.filter( + dsl::payment_id + .eq(pid.to_owned()) + .or(dsl::refund_id.eq(ref_id.to_owned())), + ); }; if !search_by_pay_or_ref_id { diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index f26cf876ade..15f1aa24370 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -871,6 +871,12 @@ impl PaymentIntentInterface for crate::RouterStore { query = query.filter(pi_dsl::customer_id.eq(customer_id.clone())); } + if let Some(merchant_order_reference_id) = ¶ms.merchant_order_reference_id { + query = query.filter( + pi_dsl::merchant_order_reference_id.eq(merchant_order_reference_id.clone()), + ) + } + if let Some(profile_id) = ¶ms.profile_id { query = query.filter(pi_dsl::profile_id.eq_any(profile_id.clone())); } @@ -1041,6 +1047,11 @@ impl PaymentIntentInterface for crate::RouterStore { if let Some(customer_id) = ¶ms.customer_id { query = query.filter(pi_dsl::customer_id.eq(customer_id.clone())); } + if let Some(merchant_order_reference_id) = ¶ms.merchant_order_reference_id { + query = query.filter( + pi_dsl::merchant_order_reference_id.eq(merchant_order_reference_id.clone()), + ) + } if let Some(profile_id) = ¶ms.profile_id { query = query.filter(pi_dsl::profile_id.eq_any(profile_id.clone())); } From 00686cc17fa20635e5fc04130aefb08a7c6c8cfc Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:05:33 +0530 Subject: [PATCH 08/51] ci(cypress): use ubuntu runner (#6655) --- .github/workflows/cypress-tests-runner.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index 84680c945e1..e07b4e48d6f 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -24,7 +24,7 @@ env: jobs: runner: name: Run Cypress tests - runs-on: hyperswitch-runners + runs-on: ubuntu-latest services: redis: @@ -190,7 +190,7 @@ jobs: ROUTER__SERVER__WORKERS: 4 shell: bash -leuo pipefail {0} run: | - scripts/execute_cypress.sh --parallel 3 + scripts/execute_cypress.sh kill "${{ env.PID }}" From 68876811a8817cdec09be407fbbbbf7f19992565 Mon Sep 17 00:00:00 2001 From: awasthi21 <107559116+awasthi21@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:42:51 +0530 Subject: [PATCH 09/51] feat(connector): [Elavon] Implement cards Flow (#6485) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .typos.toml | 1 + Cargo.lock | 1 + api-reference-v2/openapi_spec.json | 2 + api-reference/openapi_spec.json | 2 + config/config.example.toml | 2 +- config/deployments/integration_test.toml | 2 +- config/deployments/production.toml | 2 +- config/deployments/sandbox.toml | 2 +- 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 | 2 + .../connector_configs/toml/development.toml | 44 +- crates/connector_configs/toml/production.toml | 44 +- crates/connector_configs/toml/sandbox.toml | 44 +- crates/hyperswitch_connectors/Cargo.toml | 1 + .../src/connectors/elavon.rs | 226 ++++--- .../src/connectors/elavon/transformers.rs | 631 ++++++++++++++---- crates/hyperswitch_connectors/src/utils.rs | 17 + .../src/router_request_types.rs | 1 + .../payment_connector_required_fields.rs | 114 ++++ crates/router/src/core/admin.rs | 4 + crates/router/src/core/utils.rs | 1 + crates/router/src/types/api.rs | 4 +- crates/router/src/types/transformers.rs | 2 +- crates/router/tests/connectors/utils.rs | 2 + .../cypress/e2e/PaymentUtils/Elavon.js | 571 ++++++++++++++++ .../cypress/e2e/PaymentUtils/Utils.js | 2 + loadtest/config/development.toml | 2 +- 30 files changed, 1498 insertions(+), 235 deletions(-) create mode 100644 cypress-tests/cypress/e2e/PaymentUtils/Elavon.js diff --git a/.typos.toml b/.typos.toml index 109e411884a..983ead3f75d 100644 --- a/.typos.toml +++ b/.typos.toml @@ -19,6 +19,7 @@ HypoNoeLbFurNiederosterreichUWien = "HypoNoeLbFurNiederosterreichUWien" hypo_noe_lb_fur_niederosterreich_u_wien = "hypo_noe_lb_fur_niederosterreich_u_wien" IOT = "IOT" # British Indian Ocean Territory country code klick = "klick" # Swedish word for clicks +FPR = "FPR" # Fraud Prevention Rules LSO = "LSO" # Lesotho country code NAM = "NAM" # Namibia country code ND = "ND" # North Dakota state code diff --git a/Cargo.lock b/Cargo.lock index a2ede8c08c1..4118ea29c2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4048,6 +4048,7 @@ dependencies = [ "mime", "once_cell", "qrcode", + "quick-xml", "rand", "regex", "reqwest 0.11.27", diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 2a5b1cfa310..1494908c5fc 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -5911,6 +5911,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", @@ -18123,6 +18124,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index e029bef9453..f624b4f68dd 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -8743,6 +8743,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", @@ -22803,6 +22804,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", diff --git a/config/config.example.toml b/config/config.example.toml index 08a32035c4b..e55665ffc70 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -207,7 +207,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index e1ea99bc8d1..5f4de111ac8 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -49,7 +49,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index e5f90646058..61738f04cdb 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -53,7 +53,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.convergepay.com" +elavon.base_url = "https://api.convergepay.com/VirtualMerchant/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com" fiuu.base_url = "https://pay.merchant.razer.com/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 4421d1b1e96..24c7642200c 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -53,7 +53,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/development.toml b/config/development.toml index a58497f4a51..630cc4a2303 100644 --- a/config/development.toml +++ b/config/development.toml @@ -222,7 +222,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index c293a7ac821..354af98dbad 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -137,7 +137,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 11b38400920..fe2c4f037bf 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -74,6 +74,7 @@ pub enum Connector { Digitalvirgo, Dlocal, Ebanx, + Elavon, Fiserv, Fiservemea, Fiuu, @@ -216,6 +217,7 @@ impl Connector { | Self::Digitalvirgo | Self::Dlocal | Self::Ebanx + | Self::Elavon | Self::Fiserv | Self::Fiservemea | Self::Fiuu diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index e75a4038c07..9d17cdb61d4 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -71,6 +71,7 @@ pub enum RoutableConnectors { Digitalvirgo, Dlocal, Ebanx, + Elavon, Fiserv, Fiservemea, Fiuu, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index b1cf0bd9f9c..65b286f8423 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -182,6 +182,7 @@ pub struct ConnectorConfig { pub digitalvirgo: Option, pub dlocal: Option, pub ebanx_payout: Option, + pub elavon: Option, pub fiserv: Option, pub fiservemea: Option, pub fiuu: Option, @@ -346,6 +347,7 @@ impl ConnectorConfig { Connector::Digitalvirgo => Ok(connector_data.digitalvirgo), Connector::Dlocal => Ok(connector_data.dlocal), Connector::Ebanx => Ok(connector_data.ebanx_payout), + Connector::Elavon => Ok(connector_data.elavon), Connector::Fiserv => Ok(connector_data.fiserv), Connector::Fiservemea => Ok(connector_data.fiservemea), Connector::Fiuu => Ok(connector_data.fiuu), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 5d41efc1a50..8ed44df888f 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -4312,4 +4312,46 @@ type="Radio" options=["Hyperswitch"] [fiuu.connector_webhook_details] -merchant_secret="Source verification key" \ No newline at end of file +merchant_secret="Source verification key" + +[elavon] +[[elavon.credit]] + payment_method_type = "Mastercard" +[[elavon.credit]] + payment_method_type = "Visa" +[[elavon.credit]] + payment_method_type = "Interac" +[[elavon.credit]] + payment_method_type = "AmericanExpress" +[[elavon.credit]] + payment_method_type = "JCB" +[[elavon.credit]] + payment_method_type = "DinersClub" +[[elavon.credit]] + payment_method_type = "Discover" +[[elavon.credit]] + payment_method_type = "CartesBancaires" +[[elavon.credit]] + payment_method_type = "UnionPay" +[[elavon.debit]] + payment_method_type = "Mastercard" +[[elavon.debit]] + payment_method_type = "Visa" +[[elavon.debit]] + payment_method_type = "Interac" +[[elavon.debit]] + payment_method_type = "AmericanExpress" +[[elavon.debit]] + payment_method_type = "JCB" +[[elavon.debit]] + payment_method_type = "DinersClub" +[[elavon.debit]] + payment_method_type = "Discover" +[[elavon.debit]] + payment_method_type = "CartesBancaires" +[[elavon.debit]] + payment_method_type = "UnionPay" +[elavon.connector_auth.SignatureKey] +api_key="Account Id" +key1="User ID" +api_secret="Pin" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index cc501a4c101..887a1398b48 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -3270,4 +3270,46 @@ type="Radio" options=["Hyperswitch"] [fiuu.connector_webhook_details] -merchant_secret="Source verification key" \ No newline at end of file +merchant_secret="Source verification key" + +[elavon] +[[elavon.credit]] + payment_method_type = "Mastercard" +[[elavon.credit]] + payment_method_type = "Visa" +[[elavon.credit]] + payment_method_type = "Interac" +[[elavon.credit]] + payment_method_type = "AmericanExpress" +[[elavon.credit]] + payment_method_type = "JCB" +[[elavon.credit]] + payment_method_type = "DinersClub" +[[elavon.credit]] + payment_method_type = "Discover" +[[elavon.credit]] + payment_method_type = "CartesBancaires" +[[elavon.credit]] + payment_method_type = "UnionPay" +[[elavon.debit]] + payment_method_type = "Mastercard" +[[elavon.debit]] + payment_method_type = "Visa" +[[elavon.debit]] + payment_method_type = "Interac" +[[elavon.debit]] + payment_method_type = "AmericanExpress" +[[elavon.debit]] + payment_method_type = "JCB" +[[elavon.debit]] + payment_method_type = "DinersClub" +[[elavon.debit]] + payment_method_type = "Discover" +[[elavon.debit]] + payment_method_type = "CartesBancaires" +[[elavon.debit]] + payment_method_type = "UnionPay" +[elavon.connector_auth.SignatureKey] +api_key="Account Id" +key1="User ID" +api_secret="Pin" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 5462c121c37..57cb9105042 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -4276,4 +4276,46 @@ type="Radio" options=["Hyperswitch"] [fiuu.connector_webhook_details] -merchant_secret="Source verification key" \ No newline at end of file +merchant_secret="Source verification key" + +[elavon] +[[elavon.credit]] + payment_method_type = "Mastercard" +[[elavon.credit]] + payment_method_type = "Visa" +[[elavon.credit]] + payment_method_type = "Interac" +[[elavon.credit]] + payment_method_type = "AmericanExpress" +[[elavon.credit]] + payment_method_type = "JCB" +[[elavon.credit]] + payment_method_type = "DinersClub" +[[elavon.credit]] + payment_method_type = "Discover" +[[elavon.credit]] + payment_method_type = "CartesBancaires" +[[elavon.credit]] + payment_method_type = "UnionPay" +[[elavon.debit]] + payment_method_type = "Mastercard" +[[elavon.debit]] + payment_method_type = "Visa" +[[elavon.debit]] + payment_method_type = "Interac" +[[elavon.debit]] + payment_method_type = "AmericanExpress" +[[elavon.debit]] + payment_method_type = "JCB" +[[elavon.debit]] + payment_method_type = "DinersClub" +[[elavon.debit]] + payment_method_type = "Discover" +[[elavon.debit]] + payment_method_type = "CartesBancaires" +[[elavon.debit]] + payment_method_type = "UnionPay" +[elavon.connector_auth.SignatureKey] +api_key="Account Id" +key1="User ID" +api_secret="Pin" \ No newline at end of file diff --git a/crates/hyperswitch_connectors/Cargo.toml b/crates/hyperswitch_connectors/Cargo.toml index ca9d338920f..5f4def7587a 100644 --- a/crates/hyperswitch_connectors/Cargo.toml +++ b/crates/hyperswitch_connectors/Cargo.toml @@ -23,6 +23,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] } mime = "0.3.17" once_cell = "1.19.0" qrcode = "0.14.0" +quick-xml = { version = "0.31.0", features = ["serialize"] } rand = "0.8.5" regex = "1.10.4" reqwest = { version = "0.11.27" } diff --git a/crates/hyperswitch_connectors/src/connectors/elavon.rs b/crates/hyperswitch_connectors/src/connectors/elavon.rs index 7e4408d301f..b236e3a604c 100644 --- a/crates/hyperswitch_connectors/src/connectors/elavon.rs +++ b/crates/hyperswitch_connectors/src/connectors/elavon.rs @@ -1,14 +1,16 @@ pub mod transformers; +use std::{collections::HashMap, str}; +use common_enums::{CaptureMethod, PaymentMethodType}; use common_utils::{ errors::CustomResult, - ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::report; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + payment_method_data::PaymentMethodData, + router_data::{AccessToken, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, @@ -21,8 +23,8 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, }, }; use hyperswitch_interfaces::{ @@ -33,20 +35,40 @@ use hyperswitch_interfaces::{ types::{self, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{Secret, WithoutType}; +use serde::Serialize; use transformers as elavon; -use crate::{constants::headers, types::ResponseRouterData, utils}; +use crate::{ + constants::headers, + types::ResponseRouterData, + utils::{self, PaymentMethodDataType}, +}; +pub fn struct_to_xml( + item: &T, +) -> Result>, errors::ConnectorError> { + let xml_content = quick_xml::se::to_string_with_root("txn", &item).map_err(|e| { + router_env::logger::error!("Error serializing Struct: {:?}", e); + errors::ConnectorError::ResponseDeserializationFailed + })?; + + let mut result = HashMap::new(); + result.insert( + "xmldata".to_string(), + Secret::<_, WithoutType>::new(xml_content), + ); + Ok(result) +} #[derive(Clone)] pub struct Elavon { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Elavon { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &StringMajorUnitForConnector, } } } @@ -96,62 +118,18 @@ impl ConnectorCommon for Elavon { 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" + "application/x-www-form-urlencoded" } fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { connectors.elavon.base_url.as_ref() } - - fn get_auth_header( - &self, - auth_type: &ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = elavon::ElavonAuthType::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: elavon::ElavonErrorResponse = res - .response - .parse_struct("ElavonErrorResponse") - .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 Elavon { - //TODO: implement functions when support enabled -} - -impl ConnectorIntegration for Elavon { - //TODO: implement sessions flow -} +impl ConnectorIntegration for Elavon {} impl ConnectorIntegration for Elavon {} @@ -173,9 +151,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) } fn get_request_body( @@ -191,7 +169,10 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: elavon::ElavonPaymentsResponse = res - .response - .parse_struct("Elavon PaymentsAuthorizeResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonPaymentsResponse = + utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -260,9 +239,21 @@ impl ConnectorIntegration for Ela fn get_url( &self, _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &PaymentsSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = elavon::SyncRequest::try_from(req)?; + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -272,9 +263,12 @@ impl ConnectorIntegration for Ela ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Get) + .method(Method::Post) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() + .set_body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) .build(), )) @@ -286,10 +280,7 @@ impl ConnectorIntegration for Ela event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: elavon::ElavonPaymentsResponse = res - .response - .parse_struct("elavon PaymentsSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonSyncResponse = utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -324,17 +315,27 @@ impl ConnectorIntegration fo fn get_url( &self, _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &PaymentsCaptureRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + let connector_router_data = elavon::ElavonRouterData::from((amount, req)); + let connector_req = elavon::PaymentsCaptureRequest::try_from(&connector_router_data)?; + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -363,10 +364,8 @@ impl ConnectorIntegration fo event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: elavon::ElavonPaymentsResponse = res - .response - .parse_struct("Elavon PaymentsCaptureResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonPaymentsResponse = + utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -385,7 +384,15 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Elavon {} +impl ConnectorIntegration for Elavon { + fn build_request( + &self, + _req: &PaymentsCancelRouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Cancel/Void flow".to_string()).into()) + } +} impl ConnectorIntegration for Elavon { fn get_headers( @@ -403,9 +410,9 @@ impl ConnectorIntegration for Elavon fn get_url( &self, _req: &RefundsRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) } fn get_request_body( @@ -421,7 +428,10 @@ impl ConnectorIntegration for Elavon let connector_router_data = elavon::ElavonRouterData::from((refund_amount, req)); let connector_req = elavon::ElavonRefundRequest::try_from(&connector_router_data)?; - Ok(RequestContent::Json(Box::new(connector_req))) + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -449,10 +459,8 @@ impl ConnectorIntegration for Elavon event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: elavon::RefundResponse = - res.response - .parse_struct("elavon RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonPaymentsResponse = + utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -487,9 +495,20 @@ impl ConnectorIntegration for Elavon { fn get_url( &self, _req: &RefundSyncRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &RefundSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = elavon::SyncRequest::try_from(req)?; + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -499,7 +518,7 @@ impl ConnectorIntegration for Elavon { ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Get) + .method(Method::Post) .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) @@ -516,10 +535,7 @@ impl ConnectorIntegration for Elavon { event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: elavon::RefundResponse = res - .response - .parse_struct("elavon RefundSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonSyncResponse = utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -561,3 +577,27 @@ impl webhooks::IncomingWebhook for Elavon { Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } } + +impl ConnectorValidation for Elavon { + fn validate_capture_method( + &self, + capture_method: Option, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + CaptureMethod::Automatic | CaptureMethod::Manual => Ok(()), + CaptureMethod::ManualMultiple | CaptureMethod::Scheduled => Err( + utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } + fn validate_mandate_payment( + &self, + pm_type: Option, + pm_data: PaymentMethodData, + ) -> CustomResult<(), errors::ConnectorError> { + let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]); + utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id()) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs b/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs index 1dbf03ea275..67561acf8db 100644 --- a/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs @@ -1,52 +1,92 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use cards::CardNumber; +use common_enums::{enums, Currency}; +use common_utils::{pii::Email, types::StringMajorUnit}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + router_data::{ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + router_request_types::{PaymentsAuthorizeData, ResponseId}, + router_response_types::{MandateReference, PaymentsResponseData, RefundsResponseData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, + RefundSyncRouterData, RefundsRouterData, + }, }; use hyperswitch_interfaces::errors; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + types::{ + PaymentsCaptureResponseRouterData, PaymentsSyncResponseRouterData, + RefundsResponseRouterData, ResponseRouterData, + }, + utils::{CardData, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData as _}, }; -//TODO: Fill the struct with respective fields pub struct ElavonRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: StringMajorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for ElavonRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(StringMajorUnit, T)> for ElavonRouterData { + fn from((amount, item): (StringMajorUnit, T)) -> Self { Self { amount, router_data: item, } } } - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] -pub struct ElavonPaymentsRequest { - amount: StringMinorUnit, - card: ElavonCard, +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum TransactionType { + CcSale, + CcAuthOnly, + CcComplete, + CcReturn, + TxnQuery, +} +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "UPPERCASE")] +pub enum SyncTransactionType { + Sale, + AuthOnly, + Return, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct ElavonCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum ElavonPaymentsRequest { + Card(CardPaymentRequest), + MandatePayment(MandatePaymentRequest), +} +#[derive(Debug, Serialize)] +pub struct CardPaymentRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_card_number: CardNumber, + pub ssl_exp_date: Secret, + pub ssl_cvv2cvc2: Secret, + pub ssl_email: Email, + #[serde(skip_serializing_if = "Option::is_none")] + pub ssl_add_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ssl_get_token: Option, + pub ssl_transaction_currency: Currency, +} +#[derive(Debug, Serialize)] +pub struct MandatePaymentRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_email: Email, + pub ssl_token: Secret, } impl TryFrom<&ElavonRouterData<&PaymentsAuthorizeRouterData>> for ElavonPaymentsRequest { @@ -54,175 +94,506 @@ impl TryFrom<&ElavonRouterData<&PaymentsAuthorizeRouterData>> for ElavonPayments fn try_from( item: &ElavonRouterData<&PaymentsAuthorizeRouterData>, ) -> Result { + let auth = ElavonAuthType::try_from(&item.router_data.connector_auth_type)?; match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = ElavonCard { - 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, - }) - } + PaymentMethodData::Card(req_card) => Ok(Self::Card(CardPaymentRequest { + ssl_transaction_type: match item.router_data.request.is_auto_capture()? { + true => TransactionType::CcSale, + false => TransactionType::CcAuthOnly, + }, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + ssl_amount: item.amount.clone(), + ssl_card_number: req_card.card_number.clone(), + ssl_exp_date: req_card.get_expiry_date_as_mmyy()?, + ssl_cvv2cvc2: req_card.card_cvc, + ssl_email: item.router_data.get_billing_email()?, + ssl_add_token: match item.router_data.request.is_mandate_payment() { + true => Some("Y".to_string()), + false => None, + }, + ssl_get_token: match item.router_data.request.is_mandate_payment() { + true => Some("Y".to_string()), + false => None, + }, + ssl_transaction_currency: item.router_data.request.currency, + })), + PaymentMethodData::MandatePayment => Ok(Self::MandatePayment(MandatePaymentRequest { + ssl_transaction_type: match item.router_data.request.is_auto_capture()? { + true => TransactionType::CcSale, + false => TransactionType::CcAuthOnly, + }, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + ssl_amount: item.amount.clone(), + ssl_email: item.router_data.get_billing_email()?, + ssl_token: Secret::new(item.router_data.request.get_connector_mandate_id()?), + })), _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), } } } -//TODO: Fill the struct with respective fields -// Auth Struct pub struct ElavonAuthType { - pub(super) api_key: Secret, + pub(super) account_id: Secret, + pub(super) user_id: Secret, + pub(super) pin: Secret, } impl TryFrom<&ConnectorAuthType> for ElavonAuthType { 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(), + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self { + account_id: api_key.to_owned(), + user_id: key1.to_owned(), + pin: api_secret.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 ElavonPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for common_enums::AttemptStatus { - fn from(item: ElavonPaymentStatus) -> Self { - match item { - ElavonPaymentStatus::Succeeded => Self::Charged, - ElavonPaymentStatus::Failed => Self::Failure, - ElavonPaymentStatus::Processing => Self::Authorizing, - } - } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +enum SslResult { + #[serde(rename = "0")] + ImportedBatchFile, + #[serde(other)] + DeclineOrUnauthorized, } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ElavonPaymentsResponse { - status: ElavonPaymentStatus, - id: String, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ElavonPaymentsResponse { + #[serde(rename = "txn")] + Success(PaymentResponse), + #[serde(rename = "txn")] + Error(ElavonErrorResponse), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ElavonErrorResponse { + error_code: String, + error_message: String, + error_name: String, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentResponse { + ssl_result: SslResult, + ssl_txn_id: String, + ssl_result_message: String, + ssl_token: Option>, } -impl TryFrom> - for RouterData +impl + TryFrom< + ResponseRouterData, + > for RouterData { type Error = error_stack::Report; fn try_from( - item: ResponseRouterData, + item: ResponseRouterData< + F, + ElavonPaymentsResponse, + PaymentsAuthorizeData, + PaymentsResponseData, + >, ) -> 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, + let status = get_payment_status(&item.response, item.data.request.is_auto_capture()?); + let response = match &item.response { + ElavonPaymentsResponse::Error(error) => Err(ErrorResponse { + code: error.error_code.clone(), + message: error.error_message.clone(), + reason: Some(error.error_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, }), + ElavonPaymentsResponse::Success(response) => { + if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: response.ssl_result_message.clone(), + message: response.ssl_result_message.clone(), + reason: Some(response.ssl_result_message.clone()), + attempt_status: None, + connector_transaction_id: Some(response.ssl_txn_id.clone()), + status_code: item.http_code, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + response.ssl_txn_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(Some(MandateReference { + connector_mandate_id: response + .ssl_token + .as_ref() + .map(|secret| secret.clone().expose()), + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + })), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(response.ssl_txn_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }) + } + } + }; + Ok(Self { + status, + response, ..item.data }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] +pub enum TransactionSyncStatus { + PEN, // Pended + OPN, // Unpended / release / open + REV, // Review + STL, // Settled + PST, // Failed due to post-auth rule + FPR, // Failed due to fraud prevention rules + PRE, // Failed due to pre-auth rule +} + +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] +pub struct PaymentsCaptureRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_txn_id: String, +} +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] +pub struct PaymentsVoidRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_txn_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] pub struct ElavonRefundRequest { - pub amount: StringMinorUnit, + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_txn_id: String, } +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] +pub struct SyncRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_txn_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "txn")] +pub struct ElavonSyncResponse { + pub ssl_trans_status: TransactionSyncStatus, + pub ssl_transaction_type: SyncTransactionType, + pub ssl_txn_id: String, +} +impl TryFrom<&RefundSyncRouterData> for SyncRequest { + type Error = error_stack::Report; + fn try_from(item: &RefundSyncRouterData) -> Result { + let auth = ElavonAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + ssl_txn_id: item.request.get_connector_refund_id()?, + ssl_transaction_type: TransactionType::TxnQuery, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + }) + } +} +impl TryFrom<&PaymentsSyncRouterData> for SyncRequest { + type Error = error_stack::Report; + fn try_from(item: &PaymentsSyncRouterData) -> Result { + let auth = ElavonAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + ssl_txn_id: item + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?, + ssl_transaction_type: TransactionType::TxnQuery, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + }) + } +} impl TryFrom<&ElavonRouterData<&RefundsRouterData>> for ElavonRefundRequest { type Error = error_stack::Report; fn try_from(item: &ElavonRouterData<&RefundsRouterData>) -> Result { + let auth = ElavonAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(Self { - amount: item.amount.to_owned(), + ssl_txn_id: item.router_data.request.connector_transaction_id.clone(), + ssl_amount: item.amount.clone(), + ssl_transaction_type: TransactionType::CcReturn, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), }) } } -// 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 - } +impl TryFrom<&ElavonRouterData<&PaymentsCaptureRouterData>> for PaymentsCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &ElavonRouterData<&PaymentsCaptureRouterData>) -> Result { + let auth = ElavonAuthType::try_from(&item.router_data.connector_auth_type)?; + Ok(Self { + ssl_txn_id: item.router_data.request.connector_transaction_id.clone(), + ssl_amount: item.amount.clone(), + ssl_transaction_type: TransactionType::CcComplete, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +impl TryFrom> for PaymentsSyncRouterData { + type Error = error_stack::Report; + fn try_from( + item: PaymentsSyncResponseRouterData, + ) -> Result { + Ok(Self { + status: get_sync_status(item.data.status, &item.response), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.ssl_txn_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 + }) + } } - -impl TryFrom> for RefundsRouterData { +impl TryFrom> for RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.ssl_txn_id.clone(), + refund_status: get_refund_status(item.data.request.refund_status, &item.response), }), ..item.data }) } } -impl TryFrom> for RefundsRouterData { +impl TryFrom> + for PaymentsCaptureRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: PaymentsCaptureResponseRouterData, ) -> Result { + let status = map_payment_status(&item.response, enums::AttemptStatus::Charged); + let response = match &item.response { + ElavonPaymentsResponse::Error(error) => Err(ErrorResponse { + code: error.error_code.clone(), + message: error.error_message.clone(), + reason: Some(error.error_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ElavonPaymentsResponse::Success(response) => { + if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: response.ssl_result_message.clone(), + message: response.ssl_result_message.clone(), + reason: Some(response.ssl_result_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + response.ssl_txn_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(response.ssl_txn_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }) + } + } + }; Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + status, + response, + ..item.data + }) + } +} +impl TryFrom> + for RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + let status = enums::RefundStatus::from(&item.response); + let response = match &item.response { + ElavonPaymentsResponse::Error(error) => Err(ErrorResponse { + code: error.error_code.clone(), + message: error.error_message.clone(), + reason: Some(error.error_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, }), + ElavonPaymentsResponse::Success(response) => { + if status == enums::RefundStatus::Failure { + Err(ErrorResponse { + code: response.ssl_result_message.clone(), + message: response.ssl_result_message.clone(), + reason: Some(response.ssl_result_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }) + } else { + Ok(RefundsResponseData { + connector_refund_id: response.ssl_txn_id.clone(), + refund_status: enums::RefundStatus::from(&item.response), + }) + } + } + }; + Ok(Self { + response, ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ElavonErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, +trait ElavonResponseValidator { + fn is_successful(&self) -> bool; +} +impl ElavonResponseValidator for ElavonPaymentsResponse { + fn is_successful(&self) -> bool { + matches!(self, Self::Success(response) if response.ssl_result == SslResult::ImportedBatchFile) + } +} + +fn map_payment_status( + item: &ElavonPaymentsResponse, + success_status: enums::AttemptStatus, +) -> enums::AttemptStatus { + if item.is_successful() { + success_status + } else { + enums::AttemptStatus::Failure + } +} + +impl From<&ElavonPaymentsResponse> for enums::RefundStatus { + fn from(item: &ElavonPaymentsResponse) -> Self { + if item.is_successful() { + Self::Success + } else { + Self::Failure + } + } +} +fn get_refund_status( + prev_status: enums::RefundStatus, + item: &ElavonSyncResponse, +) -> enums::RefundStatus { + match item.ssl_trans_status { + TransactionSyncStatus::REV | TransactionSyncStatus::OPN | TransactionSyncStatus::PEN => { + prev_status + } + TransactionSyncStatus::STL => enums::RefundStatus::Success, + TransactionSyncStatus::PST | TransactionSyncStatus::FPR | TransactionSyncStatus::PRE => { + enums::RefundStatus::Failure + } + } +} +impl From<&ElavonSyncResponse> for enums::AttemptStatus { + fn from(item: &ElavonSyncResponse) -> Self { + match item.ssl_trans_status { + TransactionSyncStatus::REV + | TransactionSyncStatus::OPN + | TransactionSyncStatus::PEN => Self::Pending, + TransactionSyncStatus::STL => match item.ssl_transaction_type { + SyncTransactionType::Sale => Self::Charged, + SyncTransactionType::AuthOnly => Self::Authorized, + SyncTransactionType::Return => Self::Pending, + }, + TransactionSyncStatus::PST + | TransactionSyncStatus::FPR + | TransactionSyncStatus::PRE => Self::Failure, + } + } +} +fn get_sync_status( + prev_status: enums::AttemptStatus, + item: &ElavonSyncResponse, +) -> enums::AttemptStatus { + match item.ssl_trans_status { + TransactionSyncStatus::REV | TransactionSyncStatus::OPN | TransactionSyncStatus::PEN => { + prev_status + } + TransactionSyncStatus::STL => match item.ssl_transaction_type { + SyncTransactionType::Sale => enums::AttemptStatus::Charged, + SyncTransactionType::AuthOnly => enums::AttemptStatus::Authorized, + SyncTransactionType::Return => enums::AttemptStatus::Pending, + }, + TransactionSyncStatus::PST | TransactionSyncStatus::FPR | TransactionSyncStatus::PRE => { + enums::AttemptStatus::Failure + } + } +} + +fn get_payment_status( + item: &ElavonPaymentsResponse, + is_auto_capture: bool, +) -> enums::AttemptStatus { + if item.is_successful() { + if is_auto_capture { + enums::AttemptStatus::Charged + } else { + enums::AttemptStatus::Authorized + } + } else { + enums::AttemptStatus::Failure + } } diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index d928bbfa4eb..a7fe1cc4cd9 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -2250,3 +2250,20 @@ impl WalletData for hyperswitch_domain_models::payment_method_data::WalletData { } } } + +pub fn deserialize_xml_to_struct( + xml_data: &[u8], +) -> Result { + let response_str = std::str::from_utf8(xml_data) + .map_err(|e| { + router_env::logger::error!("Error converting response data to UTF-8: {:?}", e); + errors::ConnectorError::ResponseDeserializationFailed + })? + .trim(); + let result: T = quick_xml::de::from_str(response_str).map_err(|e| { + router_env::logger::error!("Error deserializing XML response: {:?}", e); + errors::ConnectorError::ResponseDeserializationFailed + })?; + + Ok(result) +} diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index c645d0b4b53..c282afa9d0d 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -617,6 +617,7 @@ pub struct RefundsData { pub minor_payment_amount: MinorUnit, pub minor_refund_amount: MinorUnit, pub integrity_object: Option, + pub refund_status: storage_enums::RefundStatus, } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 8d5a4fe89b9..4e80ccf027e 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -1355,6 +1355,63 @@ impl Default for settings::RequiredFields { ), } ), + ( + enums::Connector::Elavon, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Fiserv, RequiredFieldFinal { @@ -4399,6 +4456,63 @@ impl Default for settings::RequiredFields { ), } ), + ( + enums::Connector::Elavon, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Fiserv, RequiredFieldFinal { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 97a40f38ac4..a8d14187dc1 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1347,6 +1347,10 @@ impl<'a> ConnectorAuthTypeAndMetadataValidation<'a> { ebanx::transformers::EbanxAuthType::try_from(self.auth_type)?; Ok(()) } + api_enums::Connector::Elavon => { + elavon::transformers::ElavonAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Fiserv => { fiserv::transformers::FiservAuthType::try_from(self.auth_type)?; fiserv::transformers::FiservSessionObject::try_from(self.connector_meta_data)?; diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 167a4c59006..776692d6bec 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -363,6 +363,7 @@ pub async fn construct_refund_router_data<'a, F>( browser_info, charges, integrity_object: None, + refund_status: refund.refund_status, }, response: Ok(types::RefundsResponseData { diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index f7909a97195..ad40f83a554 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -410,7 +410,9 @@ impl ConnectorData { enums::Connector::Ebanx => { Ok(ConnectorEnum::Old(Box::new(connector::Ebanx::new()))) } - // enums::Connector::Elavon => Ok(ConnectorEnum::Old(Box::new(connector::Elavon))), + enums::Connector::Elavon => { + Ok(ConnectorEnum::Old(Box::new(connector::Elavon::new()))) + } enums::Connector::Fiserv => Ok(ConnectorEnum::Old(Box::new(&connector::Fiserv))), enums::Connector::Fiservemea => { Ok(ConnectorEnum::Old(Box::new(connector::Fiservemea::new()))) diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index ecec8211446..c8a9cbea1dd 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -234,7 +234,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Digitalvirgo => Self::Digitalvirgo, api_enums::Connector::Dlocal => Self::Dlocal, api_enums::Connector::Ebanx => Self::Ebanx, - // api_enums::Connector::Elavon => Self::Elavon, + api_enums::Connector::Elavon => Self::Elavon, api_enums::Connector::Fiserv => Self::Fiserv, api_enums::Connector::Fiservemea => Self::Fiservemea, api_enums::Connector::Fiuu => Self::Fiuu, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 3402b532fbf..a03148956ba 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -406,6 +406,7 @@ pub trait ConnectorActions: Connector { browser_info: None, charges: None, integrity_object: None, + refund_status: enums::RefundStatus::Pending, }), payment_info, ); @@ -1035,6 +1036,7 @@ impl Default for PaymentRefundType { browser_info: None, charges: None, integrity_object: None, + refund_status: enums::RefundStatus::Pending, }; Self(data) } diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js new file mode 100644 index 00000000000..5b985df29da --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js @@ -0,0 +1,571 @@ +const successfulNo3DSCardDetails = { + card_number: "4111111111111111", + card_exp_month: "06", + card_exp_year: "25", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; +const singleUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + single_use: { + amount: 8000, + currency: "USD", + }, + }, +}; + +const multiUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + multi_use: { + amount: 8000, + currency: "USD", + }, + }, +}; +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + billing: { + address: { + line1: "1467", + line2: "CA", + line3: "CA", + city: "Florence", + state: "Tuscany", + zip: "12345", + country: "IT", + first_name: "Max", + last_name: "Mustermann", + }, + email: "mauro.morandi@nexi.it", + phone: { + number: "9123456789", + country_code: "+91", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + No3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + billing: { + email: "mauro.morandi@nexi.it", + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + No3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + billing: { + email: "mauro.morandi@nexi.it", + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, manualPaymentPartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + manualPaymentRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + + }, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSAutoCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_type: "debit", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + phone: { + number: "9123456789", + country_code: "+91", + }, + email: "mauro.morandi@nexi.it", + }, + }, + setup_future_usage: "off_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSManualCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + phone: { + number: "9123456789", + country_code: "+91", + }, + email: "mauro.morandi@nexi.it", + }, + }, + setup_future_usage: "off_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardConfirmAutoCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardConfirmManualCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + phone: { + number: "9123456789", + country_code: "+91", + }, + email: "mauro.morandi@nexi.it", + }, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MandateSingleUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateSingleUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + Capture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6500, + amount_capturable: 0, + amount_received: 6500, + }, + }, + }, + PartialCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6500, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + Refund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + VoidAfterConfirm: { + Request: {}, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Cancel/Void flow is not implemented", + code: "IR_00" + } + } + }, + }, + PartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SyncRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + billing: { + email: "mauro.morandi@nexi.it", + }, + mandate_data: null, + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + billing: { + email: "mauro.morandi@nexi.it", + }, + currency: "USD", + mandate_data: null, + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index fc1685a6621..39d8ab6c8af 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -22,6 +22,7 @@ import { connectorDetails as wellsfargoConnectorDetails } from "./WellsFargo.js" import { connectorDetails as fiuuConnectorDetails } from "./Fiuu.js"; import { connectorDetails as worldpayConnectorDetails } from "./WorldPay.js"; import { connectorDetails as checkoutConnectorDetails } from "./Checkout.js"; +import { connectorDetails as elavonConnectorDetails } from "./Elavon.js"; const connectorDetails = { adyen: adyenConnectorDetails, @@ -39,6 +40,7 @@ const connectorDetails = { paybox: payboxConnectorDetails, paypal: paypalConnectorDetails, stripe: stripeConnectorDetails, + elavon: elavonConnectorDetails, trustpay: trustpayConnectorDetails, datatrans: datatransConnectorDetails, wellsfargo: wellsfargoConnectorDetails, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index b7a6fca7de4..8aeacaeca1f 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -103,7 +103,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" From 26c61969ebe245920ac6fff08da5e990d741a236 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:21:59 +0000 Subject: [PATCH 10/51] chore(version): 2024.11.26.0 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2982af5e0db..6be9517667f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.26.0 + +### Features + +- **connector:** + - [Paypal] implement vaulting for paypal cards via zero mandates ([#5324](https://github.com/juspay/hyperswitch/pull/5324)) ([`83e8bc0`](https://github.com/juspay/hyperswitch/commit/83e8bc0775c20e9d055e65bd13a2e8b1148092e1)) + - [Elavon] Implement cards Flow ([#6485](https://github.com/juspay/hyperswitch/pull/6485)) ([`6887681`](https://github.com/juspay/hyperswitch/commit/68876811a8817cdec09be407fbbbbf7f19992565)) +- **core:** Add SCA exemption field ([#6578](https://github.com/juspay/hyperswitch/pull/6578)) ([`2b8eb09`](https://github.com/juspay/hyperswitch/commit/2b8eb09a16040957ac369c48e6095c343207f0d3)) +- **payments:** Add merchant order ref id filter ([#6630](https://github.com/juspay/hyperswitch/pull/6630)) ([`57e64c2`](https://github.com/juspay/hyperswitch/commit/57e64c26ca4251b493c87bfe93799faaab4ffa89)) + +### Miscellaneous Tasks + +- **deps:** Update cypress packages to address CVE ([#6624](https://github.com/juspay/hyperswitch/pull/6624)) ([`0db3aed`](https://github.com/juspay/hyperswitch/commit/0db3aed1533856b9892369d7bb2430d90d091756)) + +**Full Changelog:** [`2024.11.25.0...2024.11.26.0`](https://github.com/juspay/hyperswitch/compare/2024.11.25.0...2024.11.26.0) + +- - - + ## 2024.11.25.0 ### Features From 710186f035c92a919e8f5a49565c6f8908f1803f Mon Sep 17 00:00:00 2001 From: sweta-kumari-sharma <77436883+Sweta-Kumari-Sharma@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:44:44 +0530 Subject: [PATCH 11/51] feat(connector): [INESPAY] add Connector Template Code (#6614) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + 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 | 2 + .../hyperswitch_connectors/src/connectors.rs | 11 +- .../src/connectors/inespay.rs | 563 ++++++++++++++++++ .../src/connectors/inespay/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 | 16 +- crates/router/src/core/admin.rs | 4 + .../connector_integration_v2_impls.rs | 3 + crates/router/src/core/payments/flows.rs | 3 + crates/router/src/types/api.rs | 3 + crates/router/src/types/transformers.rs | 1 + crates/router/tests/connectors/inespay.rs | 421 +++++++++++++ crates/router/tests/connectors/main.rs | 1 + .../router/tests/connectors/sample_auth.toml | 3 + crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + scripts/add_connector.sh | 2 +- 27 files changed, 1316 insertions(+), 14 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/inespay.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/inespay/transformers.rs create mode 100644 crates/router/tests/connectors/inespay.rs diff --git a/config/config.example.toml b/config/config.example.toml index e55665ffc70..191f2ba7f8b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -220,6 +220,7 @@ gocardless.base_url = "https://api-sandbox.gocardless.com" gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayments.net" helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 5f4de111ac8..00a544dc565 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -62,6 +62,7 @@ gocardless.base_url = "https://api-sandbox.gocardless.com" gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayments.net" helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 61738f04cdb..0fe9095d280 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -66,6 +66,7 @@ gocardless.base_url = "https://api.gocardless.com" gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayments.net" helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://iata-pay.iata.org/api/v1" +inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://secure.api.itau/" jpmorgan.base_url = "https://api-ms.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.klarna.com/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 24c7642200c..82c347ae389 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -66,6 +66,7 @@ gocardless.base_url = "https://api-sandbox.gocardless.com" gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayments.net" helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" diff --git a/config/development.toml b/config/development.toml index 630cc4a2303..ee6ea5dab0b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -130,6 +130,7 @@ cards = [ "gpayments", "helcim", "iatapay", + "inespay", "itaubank", "jpmorgan", "mollie", @@ -235,6 +236,7 @@ gocardless.base_url = "https://api-sandbox.gocardless.com" gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayments.net" helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 354af98dbad..ed0ede98d94 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -150,6 +150,7 @@ gocardless.base_url = "https://api-sandbox.gocardless.com" gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayments.net" helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" @@ -249,6 +250,7 @@ cards = [ "gpayments", "helcim", "iatapay", + "inespay", "itaubank", "jpmorgan", "mollie", diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index fe2c4f037bf..783ecb12b48 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -84,6 +84,7 @@ pub enum Connector { Gocardless, Gpayments, Helcim, + // Inespay, Iatapay, Itaubank, //Jpmorgan, @@ -228,6 +229,7 @@ impl Connector { | Self::Gpayments | Self::Helcim | Self::Iatapay + // | Self::Inespay | Self::Itaubank //| Self::Jpmorgan | Self::Klarna diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 9d17cdb61d4..c3bbf6e078f 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -81,6 +81,7 @@ pub enum RoutableConnectors { Gocardless, Helcim, Iatapay, + // Inespay, Itaubank, //Jpmorgan, Klarna, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 65b286f8423..0e68b04d27b 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -192,6 +192,7 @@ pub struct ConnectorConfig { pub gocardless: Option, pub gpayments: Option, pub helcim: Option, + // pub inespay: Option, pub klarna: Option, pub mifinity: Option, pub mollie: Option, @@ -357,6 +358,7 @@ impl ConnectorConfig { Connector::Gocardless => Ok(connector_data.gocardless), Connector::Gpayments => Ok(connector_data.gpayments), Connector::Helcim => Ok(connector_data.helcim), + // Connector::Inespay => Ok(connector_data.inespay), Connector::Klarna => Ok(connector_data.klarna), Connector::Mifinity => Ok(connector_data.mifinity), Connector::Mollie => Ok(connector_data.mollie), diff --git a/crates/hyperswitch_connectors/src/connectors.rs b/crates/hyperswitch_connectors/src/connectors.rs index b764ab5b441..d1cdb85e57f 100644 --- a/crates/hyperswitch_connectors/src/connectors.rs +++ b/crates/hyperswitch_connectors/src/connectors.rs @@ -16,6 +16,7 @@ pub mod fiuu; pub mod forte; pub mod globepay; pub mod helcim; +pub mod inespay; pub mod jpmorgan; pub mod mollie; pub mod multisafepay; @@ -45,9 +46,9 @@ pub use self::{ bitpay::Bitpay, cashtocode::Cashtocode, coinbase::Coinbase, cryptopay::Cryptopay, deutschebank::Deutschebank, digitalvirgo::Digitalvirgo, dlocal::Dlocal, elavon::Elavon, fiserv::Fiserv, fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, globepay::Globepay, - helcim::Helcim, jpmorgan::Jpmorgan, mollie::Mollie, multisafepay::Multisafepay, - 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, xendit::Xendit, zen::Zen, zsl::Zsl, + helcim::Helcim, inespay::Inespay, jpmorgan::Jpmorgan, mollie::Mollie, + multisafepay::Multisafepay, 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, xendit::Xendit, zen::Zen, zsl::Zsl, }; diff --git a/crates/hyperswitch_connectors/src/connectors/inespay.rs b/crates/hyperswitch_connectors/src/connectors/inespay.rs new file mode 100644 index 00000000000..89bf50c60ca --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/inespay.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 inespay; + +use crate::{constants::headers, types::ResponseRouterData, utils}; + +#[derive(Clone)] +pub struct Inespay { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Inespay { + pub fn new() -> &'static Self { + &Self { + amount_converter: &StringMinorUnitForConnector, + } + } +} + +impl api::Payment for Inespay {} +impl api::PaymentSession for Inespay {} +impl api::ConnectorAccessToken for Inespay {} +impl api::MandateSetup for Inespay {} +impl api::PaymentAuthorize for Inespay {} +impl api::PaymentSync for Inespay {} +impl api::PaymentCapture for Inespay {} +impl api::PaymentVoid for Inespay {} +impl api::Refund for Inespay {} +impl api::RefundExecute for Inespay {} +impl api::RefundSync for Inespay {} +impl api::PaymentToken for Inespay {} + +impl ConnectorIntegration + for Inespay +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Inespay +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 Inespay { + fn id(&self) -> &'static str { + "inespay" + } + + 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.inespay.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = inespay::InespayAuthType::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: inespay::InespayErrorResponse = res + .response + .parse_struct("InespayErrorResponse") + .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 Inespay { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration for Inespay { + //TODO: implement sessions flow +} + +impl ConnectorIntegration for Inespay {} + +impl ConnectorIntegration for Inespay {} + +impl ConnectorIntegration for Inespay { + 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 = inespay::InespayRouterData::from((amount, req)); + let connector_req = inespay::InespayPaymentsRequest::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: inespay::InespayPaymentsResponse = res + .response + .parse_struct("Inespay 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 Inespay { + 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: inespay::InespayPaymentsResponse = res + .response + .parse_struct("inespay 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 Inespay { + 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: inespay::InespayPaymentsResponse = res + .response + .parse_struct("Inespay 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 Inespay {} + +impl ConnectorIntegration for Inespay { + 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 = inespay::InespayRouterData::from((refund_amount, req)); + let connector_req = inespay::InespayRefundRequest::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: inespay::RefundResponse = res + .response + .parse_struct("inespay 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 Inespay { + 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: inespay::RefundResponse = res + .response + .parse_struct("inespay 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 Inespay { + 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/inespay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/inespay/transformers.rs new file mode 100644 index 00000000000..296d76546c8 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/inespay/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 InespayRouterData { + 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 InespayRouterData { + 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 InespayPaymentsRequest { + amount: StringMinorUnit, + card: InespayCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct InespayCard { + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&InespayRouterData<&PaymentsAuthorizeRouterData>> for InespayPaymentsRequest { + type Error = error_stack::Report; + fn try_from( + item: &InespayRouterData<&PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(req_card) => { + let card = InespayCard { + 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 InespayAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&ConnectorAuthType> for InespayAuthType { + 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 InespayPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for common_enums::AttemptStatus { + fn from(item: InespayPaymentStatus) -> Self { + match item { + InespayPaymentStatus::Succeeded => Self::Charged, + InespayPaymentStatus::Failed => Self::Failure, + InespayPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct InespayPaymentsResponse { + status: InespayPaymentStatus, + 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 InespayRefundRequest { + pub amount: StringMinorUnit, +} + +impl TryFrom<&InespayRouterData<&RefundsRouterData>> for InespayRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &InespayRouterData<&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 InespayErrorResponse { + 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 bc86e713501..50b28be2b0b 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -107,6 +107,7 @@ default_imp_for_authorize_session_token!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -164,6 +165,7 @@ default_imp_for_calculate_tax!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Mollie, connectors::Multisafepay, @@ -218,6 +220,7 @@ default_imp_for_session_update!( connectors::Fiservemea, connectors::Forte, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Razorpay, connectors::Shift4, @@ -277,6 +280,7 @@ default_imp_for_post_session_tokens!( connectors::Fiservemea, connectors::Forte, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Razorpay, connectors::Shift4, @@ -335,6 +339,7 @@ default_imp_for_complete_authorize!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Multisafepay, connectors::Nomupay, @@ -389,6 +394,7 @@ default_imp_for_incremental_authorization!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -448,6 +454,7 @@ default_imp_for_create_customer!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Mollie, connectors::Multisafepay, @@ -505,6 +512,7 @@ default_imp_for_connector_redirect_response!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Multisafepay, connectors::Nexinets, @@ -559,6 +567,7 @@ default_imp_for_pre_processing_steps!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -616,6 +625,7 @@ default_imp_for_post_processing_steps!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -675,6 +685,7 @@ default_imp_for_approve!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -734,6 +745,7 @@ default_imp_for_reject!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -793,6 +805,7 @@ default_imp_for_webhook_source_verification!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -853,6 +866,7 @@ default_imp_for_accept_dispute!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -912,6 +926,7 @@ default_imp_for_submit_evidence!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -970,6 +985,7 @@ default_imp_for_defend_dispute!( connectors::Fiuu, connectors::Forte, connectors::Globepay, + connectors::Inespay, connectors::Jpmorgan, connectors::Helcim, connectors::Nomupay, @@ -1039,6 +1055,7 @@ default_imp_for_file_upload!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1091,6 +1108,7 @@ default_imp_for_payouts!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Mollie, connectors::Multisafepay, @@ -1151,6 +1169,7 @@ default_imp_for_payouts_create!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1212,6 +1231,7 @@ default_imp_for_payouts_retrieve!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1273,6 +1293,7 @@ default_imp_for_payouts_eligibility!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1334,6 +1355,7 @@ default_imp_for_payouts_fulfill!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1395,6 +1417,7 @@ default_imp_for_payouts_cancel!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1456,6 +1479,7 @@ default_imp_for_payouts_quote!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1517,6 +1541,7 @@ default_imp_for_payouts_recipient!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1578,6 +1603,7 @@ default_imp_for_payouts_recipient_account!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1639,6 +1665,7 @@ default_imp_for_frm_sale!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1700,6 +1727,7 @@ default_imp_for_frm_checkout!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1761,6 +1789,7 @@ default_imp_for_frm_transaction!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1822,6 +1851,7 @@ default_imp_for_frm_fulfillment!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1883,6 +1913,7 @@ default_imp_for_frm_record_return!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1941,6 +1972,7 @@ default_imp_for_revoking_mandates!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index 75cbf192e55..7b19ca68365 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -223,6 +223,7 @@ default_imp_for_new_connector_integration_payment!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -283,6 +284,7 @@ default_imp_for_new_connector_integration_refund!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -338,6 +340,7 @@ default_imp_for_new_connector_integration_connector_access_token!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -399,6 +402,7 @@ default_imp_for_new_connector_integration_accept_dispute!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -459,6 +463,7 @@ default_imp_for_new_connector_integration_submit_evidence!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -519,6 +524,7 @@ default_imp_for_new_connector_integration_defend_dispute!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -589,6 +595,7 @@ default_imp_for_new_connector_integration_file_upload!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -651,6 +658,7 @@ default_imp_for_new_connector_integration_payouts_create!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -713,6 +721,7 @@ default_imp_for_new_connector_integration_payouts_eligibility!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -775,6 +784,7 @@ default_imp_for_new_connector_integration_payouts_fulfill!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -837,6 +847,7 @@ default_imp_for_new_connector_integration_payouts_cancel!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -899,6 +910,7 @@ default_imp_for_new_connector_integration_payouts_quote!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -961,6 +973,7 @@ default_imp_for_new_connector_integration_payouts_recipient!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1023,6 +1036,7 @@ default_imp_for_new_connector_integration_payouts_sync!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1085,6 +1099,7 @@ default_imp_for_new_connector_integration_payouts_recipient_account!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1145,6 +1160,7 @@ default_imp_for_new_connector_integration_webhook_source_verification!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1207,6 +1223,7 @@ default_imp_for_new_connector_integration_frm_sale!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1269,6 +1286,7 @@ default_imp_for_new_connector_integration_frm_checkout!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1331,6 +1349,7 @@ default_imp_for_new_connector_integration_frm_transaction!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1393,6 +1412,7 @@ default_imp_for_new_connector_integration_frm_fulfillment!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1455,6 +1475,7 @@ default_imp_for_new_connector_integration_frm_record_return!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, @@ -1514,6 +1535,7 @@ default_imp_for_new_connector_integration_revoking_mandates!( connectors::Forte, connectors::Globepay, connectors::Helcim, + connectors::Inespay, connectors::Jpmorgan, connectors::Nomupay, connectors::Novalnet, diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index 5e5eeea31b3..539b87c4808 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -47,6 +47,7 @@ pub struct Connectors { pub gpayments: ConnectorParams, pub helcim: ConnectorParams, pub iatapay: ConnectorParams, + pub inespay: ConnectorParams, pub itaubank: ConnectorParams, pub jpmorgan: ConnectorParams, pub klarna: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index e98730d006d..b6668323ba9 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -50,14 +50,14 @@ pub use hyperswitch_connectors::connectors::{ coinbase, coinbase::Coinbase, cryptopay, cryptopay::Cryptopay, deutschebank, deutschebank::Deutschebank, digitalvirgo, digitalvirgo::Digitalvirgo, dlocal, dlocal::Dlocal, elavon, elavon::Elavon, fiserv, fiserv::Fiserv, fiservemea, fiservemea::Fiservemea, fiuu, - fiuu::Fiuu, forte, forte::Forte, globepay, globepay::Globepay, helcim, helcim::Helcim, - jpmorgan, jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, multisafepay::Multisafepay, - nexinets, nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, nomupay, nomupay::Nomupay, - novalnet, novalnet::Novalnet, payeezy, payeezy::Payeezy, payu, payu::Payu, powertranz, - 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, - xendit, xendit::Xendit, zen, zen::Zen, zsl, zsl::Zsl, + fiuu::Fiuu, forte, forte::Forte, globepay, globepay::Globepay, helcim, helcim::Helcim, inespay, + inespay::Inespay, jpmorgan, jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, + multisafepay::Multisafepay, nexinets, nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, + nomupay, nomupay::Nomupay, novalnet, novalnet::Novalnet, payeezy, payeezy::Payeezy, payu, + payu::Payu, powertranz, 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, xendit, xendit::Xendit, zen, zen::Zen, zsl, zsl::Zsl, }; #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index a8d14187dc1..6d4dde53082 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1393,6 +1393,10 @@ impl<'a> ConnectorAuthTypeAndMetadataValidation<'a> { iatapay::transformers::IatapayAuthType::try_from(self.auth_type)?; Ok(()) } + // api_enums::Connector::Inespay => { + // inespay::transformers::InespayAuthType::try_from(self.auth_type)?; + // Ok(()) + // } api_enums::Connector::Itaubank => { itaubank::transformers::ItaubankAuthType::try_from(self.auth_type)?; Ok(()) diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index 94c531a0963..44e8c25d67b 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -1142,6 +1142,7 @@ default_imp_for_new_connector_integration_payouts!( connector::Gpayments, connector::Helcim, connector::Iatapay, + connector::Inespay, connector::Itaubank, connector::Jpmorgan, connector::Klarna, @@ -1789,6 +1790,7 @@ default_imp_for_new_connector_integration_frm!( connector::Gpayments, connector::Helcim, connector::Iatapay, + connector::Inespay, connector::Itaubank, connector::Jpmorgan, connector::Klarna, @@ -2284,6 +2286,7 @@ default_imp_for_new_connector_integration_connector_authentication!( connector::Gpayments, connector::Helcim, connector::Iatapay, + connector::Inespay, connector::Itaubank, connector::Jpmorgan, connector::Klarna, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 1358bcedba1..9ba260f554f 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -483,6 +483,7 @@ default_imp_for_connector_request_id!( connector::Gocardless, connector::Gpayments, connector::Iatapay, + connector::Inespay, connector::Itaubank, connector::Jpmorgan, connector::Klarna, @@ -1769,6 +1770,7 @@ default_imp_for_fraud_check!( connector::Gpayments, connector::Helcim, connector::Iatapay, + connector::Inespay, connector::Itaubank, connector::Jpmorgan, connector::Klarna, @@ -2432,6 +2434,7 @@ default_imp_for_connector_authentication!( connector::Gocardless, connector::Helcim, connector::Iatapay, + connector::Inespay, connector::Itaubank, connector::Jpmorgan, connector::Klarna, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index ad40f83a554..d550c1978b2 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -434,6 +434,9 @@ impl ConnectorData { enums::Connector::Iatapay => { Ok(ConnectorEnum::Old(Box::new(connector::Iatapay::new()))) } + // enums::Connector::Inespay => { + // Ok(ConnectorEnum::Old(Box::new(connector::Inespay::new()))) + // } enums::Connector::Itaubank => { //enums::Connector::Jpmorgan => Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan))), Ok(ConnectorEnum::Old(Box::new(connector::Itaubank::new()))) diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index c8a9cbea1dd..78138757493 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -249,6 +249,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { } api_enums::Connector::Helcim => Self::Helcim, api_enums::Connector::Iatapay => Self::Iatapay, + // api_enums::Connector::Inespay => Self::Inespay, api_enums::Connector::Itaubank => Self::Itaubank, //api_enums::Connector::Jpmorgan => Self::Jpmorgan, api_enums::Connector::Klarna => Self::Klarna, diff --git a/crates/router/tests/connectors/inespay.rs b/crates/router/tests/connectors/inespay.rs new file mode 100644 index 00000000000..6fb8914aec7 --- /dev/null +++ b/crates/router/tests/connectors/inespay.rs @@ -0,0 +1,421 @@ +use hyperswitch_domain_models::payment_method_data::{Card, PaymentMethodData}; +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct InespayTest; +impl ConnectorActions for InespayTest {} +impl utils::Connector for InespayTest { + fn get_data(&self) -> api::ConnectorData { + use router::connector::Inespay; + utils::construct_connector_data_old( + Box::new(Inespay::new()), + types::Connector::Plaid, + api::GetToken::Connector, + None, + ) + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .inespay + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "inespay".to_string() + } +} + +static CONNECTOR: InespayTest = InespayTest {}; + +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: PaymentMethodData::Card(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: PaymentMethodData::Card(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: PaymentMethodData::Card(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/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 9bb079b7a16..ef3ae2d14db 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -43,6 +43,7 @@ mod gocardless; mod gpayments; mod helcim; mod iatapay; +mod inespay; mod itaubank; mod jpmorgan; mod mifinity; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index d2eec724597..120ce5e9d26 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -287,6 +287,9 @@ api_secret = "Client Key" [thunes] api_key="API Key" +[inespay] +api_key="API Key" + [jpmorgan] api_key="API Key" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 2b2dc143113..4bb348d6679 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -49,6 +49,7 @@ pub struct ConnectorAuthentication { pub gpayments: Option, pub helcim: Option, pub iatapay: Option, + pub inespay: Option, pub itaubank: Option, pub jpmorgan: Option, pub mifinity: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 8aeacaeca1f..dab85eb3cdd 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -116,6 +116,7 @@ gocardless.base_url = "https://api-sandbox.gocardless.com" gpayments.base_url = "https://{{merchant_endpoint_prefix}}-test.api.as1.gpayments.net" helcim.base_url = "https://api.helcim.com/" iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" @@ -215,6 +216,7 @@ cards = [ "gpayments", "helcim", "iatapay", + "inespay", "itaubank", "jpmorgan", "mollie", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 9f1992bfa44..e5a65128319 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 xendit 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 inespay 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 03423a1f76d324453052da985f998fd3f957ce90 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:46:56 +0530 Subject: [PATCH 12/51] feat(users): Send welcome to community email in magic link signup (#6639) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/consts/user.rs | 2 + crates/router/src/core/user.rs | 25 +- .../email/assets/welcome_to_community.html | 306 ++++++++++++++++++ crates/router/src/services/email/types.rs | 22 ++ 4 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 crates/router/src/services/email/assets/welcome_to_community.html diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index aa427992082..32ca4ad31d7 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -41,3 +41,5 @@ pub const EMAIL_SUBJECT_INVITATION: &str = "You have been invited to join Hypers pub const EMAIL_SUBJECT_MAGIC_LINK: &str = "Unlock Hyperswitch: Use Your Magic Link to Sign In"; pub const EMAIL_SUBJECT_RESET_PASSWORD: &str = "Get back to Hyperswitch - Reset Your Password Now"; pub const EMAIL_SUBJECT_NEW_PROD_INTENT: &str = "New Prod Intent"; +pub const EMAIL_SUBJECT_WELCOME_TO_COMMUNITY: &str = + "Thank you for signing up on Hyperswitch Dashboard!"; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index bff6205f5db..eedd9ac5672 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -246,26 +246,41 @@ pub async fn connect_account( ) .await?; - let email_contents = email_types::VerifyEmail { + let magic_link_email = email_types::VerifyEmail { recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, settings: state.conf.clone(), subject: consts::user::EMAIL_SUBJECT_SIGNUP, auth_id, }; - let send_email_result = state + let magic_link_result = state .email_client .compose_and_send_email( - Box::new(email_contents), + Box::new(magic_link_email), state.conf.proxy.https_url.as_ref(), ) .await; - logger::info!(?send_email_result); + logger::info!(?magic_link_result); + + let welcome_to_community_email = email_types::WelcomeToCommunity { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + subject: consts::user::EMAIL_SUBJECT_WELCOME_TO_COMMUNITY, + }; + + let welcome_email_result = state + .email_client + .compose_and_send_email( + Box::new(welcome_to_community_email), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?welcome_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { - is_email_sent: send_email_result.is_ok(), + is_email_sent: magic_link_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), }, )); diff --git a/crates/router/src/services/email/assets/welcome_to_community.html b/crates/router/src/services/email/assets/welcome_to_community.html new file mode 100644 index 00000000000..05f7fac1d55 --- /dev/null +++ b/crates/router/src/services/email/assets/welcome_to_community.html @@ -0,0 +1,306 @@ + + + + + + + Email Template + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ Follow us on + + Twitter + + + LinkedIn + +
+
+
+ + + diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index cedd17828f1..d092afdc5de 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -57,6 +57,7 @@ pub enum EmailBody { api_key_name: String, prefix: String, }, + WelcomeToCommunity, } pub mod html { @@ -145,6 +146,9 @@ Email : {user_email} prefix = prefix, expires_in = expires_in, ), + EmailBody::WelcomeToCommunity => { + include_str!("assets/welcome_to_community.html").to_string() + } } } } @@ -505,3 +509,21 @@ impl EmailData for ApiKeyExpiryReminder { }) } } + +pub struct WelcomeToCommunity { + pub recipient_email: domain::UserEmail, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for WelcomeToCommunity { + async fn get_email_data(&self) -> CustomResult { + let body = html::get_html_body(EmailBody::WelcomeToCommunity); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} From 108b1603fa44b2a56c278196edb5a1f76f5d3d03 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:47:12 +0530 Subject: [PATCH 13/51] refactor(payments_v2): use batch encryption for intent create and confirm intent (#6589) Co-authored-by: Sanchith Hegde Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../hyperswitch_domain_models/src/address.rs | 159 ++++++++++++++++++ crates/hyperswitch_domain_models/src/lib.rs | 1 + .../hyperswitch_domain_models/src/payments.rs | 39 +++-- .../src/payments/payment_attempt.rs | 70 +++++--- .../operations/payment_confirm_intent.rs | 30 +++- .../operations/payment_create_intent.rs | 71 ++++---- .../router/src/core/payments/transformers.rs | 6 +- .../src/macros/to_encryptable.rs | 16 +- 8 files changed, 307 insertions(+), 85 deletions(-) create mode 100644 crates/hyperswitch_domain_models/src/address.rs diff --git a/crates/hyperswitch_domain_models/src/address.rs b/crates/hyperswitch_domain_models/src/address.rs new file mode 100644 index 00000000000..85595c1ad9e --- /dev/null +++ b/crates/hyperswitch_domain_models/src/address.rs @@ -0,0 +1,159 @@ +use masking::{PeekInterface, Secret}; + +#[derive(Default, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct Address { + pub address: Option, + pub phone: Option, + pub email: Option, +} + +impl masking::SerializableSecret for Address {} + +impl Address { + /// Unify the address, giving priority to `self` when details are present in both + pub fn unify_address(self, other: Option<&Self>) -> Self { + let other_address_details = other.and_then(|address| address.address.as_ref()); + Self { + address: self + .address + .map(|address| address.unify_address_details(other_address_details)) + .or(other_address_details.cloned()), + email: self.email.or(other.and_then(|other| other.email.clone())), + phone: self.phone.or(other.and_then(|other| other.phone.clone())), + } + } +} + +#[derive(Clone, Default, Debug, Eq, serde::Deserialize, serde::Serialize, PartialEq)] +pub struct AddressDetails { + pub city: Option, + pub country: Option, + pub line1: Option>, + pub line2: Option>, + pub line3: Option>, + pub zip: Option>, + pub state: Option>, + pub first_name: Option>, + pub last_name: Option>, +} + +impl AddressDetails { + pub fn get_optional_full_name(&self) -> Option> { + match (self.first_name.as_ref(), self.last_name.as_ref()) { + (Some(first_name), Some(last_name)) => Some(Secret::new(format!( + "{} {}", + first_name.peek(), + last_name.peek() + ))), + (Some(name), None) | (None, Some(name)) => Some(name.to_owned()), + _ => None, + } + } + + /// Unify the address details, giving priority to `self` when details are present in both + pub fn unify_address_details(self, other: Option<&Self>) -> Self { + if let Some(other) = other { + let (first_name, last_name) = if self + .first_name + .as_ref() + .is_some_and(|first_name| !first_name.peek().trim().is_empty()) + { + (self.first_name, self.last_name) + } else { + (other.first_name.clone(), other.last_name.clone()) + }; + + Self { + first_name, + last_name, + city: self.city.or(other.city.clone()), + country: self.country.or(other.country), + line1: self.line1.or(other.line1.clone()), + line2: self.line2.or(other.line2.clone()), + line3: self.line3.or(other.line3.clone()), + zip: self.zip.or(other.zip.clone()), + state: self.state.or(other.state.clone()), + } + } else { + self + } + } +} + +#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct PhoneDetails { + pub number: Option>, + pub country_code: Option, +} + +impl From for Address { + fn from(address: api_models::payments::Address) -> Self { + Self { + address: address.address.map(AddressDetails::from), + phone: address.phone.map(PhoneDetails::from), + email: address.email, + } + } +} + +impl From for AddressDetails { + fn from(address: api_models::payments::AddressDetails) -> Self { + Self { + city: address.city, + country: address.country, + line1: address.line1, + line2: address.line2, + line3: address.line3, + zip: address.zip, + state: address.state, + first_name: address.first_name, + last_name: address.last_name, + } + } +} + +impl From for PhoneDetails { + fn from(phone: api_models::payments::PhoneDetails) -> Self { + Self { + number: phone.number, + country_code: phone.country_code, + } + } +} + +impl From
for api_models::payments::Address { + fn from(address: Address) -> Self { + Self { + address: address + .address + .map(api_models::payments::AddressDetails::from), + phone: address.phone.map(api_models::payments::PhoneDetails::from), + email: address.email, + } + } +} + +impl From for api_models::payments::AddressDetails { + fn from(address: AddressDetails) -> Self { + Self { + city: address.city, + country: address.country, + line1: address.line1, + line2: address.line2, + line3: address.line3, + zip: address.zip, + state: address.state, + first_name: address.first_name, + last_name: address.last_name, + } + } +} + +impl From for api_models::payments::PhoneDetails { + fn from(phone: PhoneDetails) -> Self { + Self { + number: phone.number, + country_code: phone.country_code, + } + } +} diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 386e0f01f38..64c6c97a0fd 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -1,3 +1,4 @@ +pub mod address; pub mod api; pub mod behaviour; pub mod business_profile; diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 276035214a4..4cb93403219 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; #[cfg(feature = "v2")] -use api_models::payments::Address; +use common_utils::ext_traits::ValueExt; use common_utils::{ self, crypto::Encryptable, @@ -28,11 +28,13 @@ use common_enums as storage_enums; use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount}; use self::payment_attempt::PaymentAttempt; +#[cfg(feature = "v1")] use crate::RemoteStorageObject; #[cfg(feature = "v2")] -use crate::{business_profile, merchant_account}; -#[cfg(feature = "v2")] -use crate::{errors, payment_method_data, ApiModelToDieselModelConvertor}; +use crate::{ + address::Address, business_profile, errors, merchant_account, payment_method_data, + ApiModelToDieselModelConvertor, +}; #[cfg(feature = "v1")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToEncryption)] @@ -349,10 +351,10 @@ pub struct PaymentIntent { pub merchant_reference_id: Option, /// The billing address for the order in a denormalized form. #[encrypt(ty = Value)] - pub billing_address: Option>>, + pub billing_address: Option>, /// The shipping address for the order in a denormalized form. #[encrypt(ty = Value)] - pub shipping_address: Option>>, + pub shipping_address: Option>, /// Capture method for the payment pub capture_method: storage_enums::CaptureMethod, /// Authentication type that is requested by the merchant for this payment. @@ -416,8 +418,7 @@ impl PaymentIntent { merchant_account: &merchant_account::MerchantAccount, profile: &business_profile::Profile, request: api_models::payments::PaymentsCreateIntentRequest, - billing_address: Option>>, - shipping_address: Option>>, + decrypted_payment_intent: DecryptedPaymentIntent, ) -> CustomResult { let connector_metadata = request .get_connector_metadata_as_value() @@ -480,8 +481,26 @@ impl PaymentIntent { frm_metadata: request.frm_metadata, customer_details: None, merchant_reference_id: request.merchant_reference_id, - billing_address, - shipping_address, + billing_address: decrypted_payment_intent + .billing_address + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode billing address")?, + shipping_address: decrypted_payment_intent + .shipping_address + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode shipping address")?, capture_method: request.capture_method.unwrap_or_default(), authentication_type: request.authentication_type.unwrap_or_default(), prerouting_algorithm: None, diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index ec1463d1b7b..4ca6084c958 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -1,6 +1,11 @@ #[cfg(all(feature = "v1", feature = "olap"))] use api_models::enums::Connector; use common_enums as storage_enums; +#[cfg(feature = "v2")] +use common_utils::{ + crypto::Encryptable, encryption::Encryption, ext_traits::ValueExt, + types::keymanager::ToEncryptable, +}; use common_utils::{ errors::{CustomResult, ValidationError}, id_type, pii, @@ -18,15 +23,19 @@ use error_stack::ResultExt; #[cfg(feature = "v2")] use masking::PeekInterface; use masking::Secret; +#[cfg(feature = "v2")] +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; +#[cfg(feature = "v2")] +use serde_json::Value; use time::PrimitiveDateTime; #[cfg(all(feature = "v1", feature = "olap"))] use super::PaymentIntent; #[cfg(feature = "v2")] -use crate::merchant_key_store::MerchantKeyStore; +use crate::type_encryption::{crypto_operation, CryptoOperation}; #[cfg(feature = "v2")] -use crate::router_response_types; +use crate::{address::Address, merchant_key_store::MerchantKeyStore, router_response_types}; use crate::{ behaviour, errors, mandates::{MandateDataType, MandateDetails}, @@ -222,7 +231,7 @@ pub struct ErrorDetails { /// Few fields which are related are grouped together for better readability and understandability. /// These fields will be flattened and stored in the database in individual columns #[cfg(feature = "v2")] -#[derive(Clone, Debug, PartialEq, serde::Serialize)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, router_derive::ToEncryption)] pub struct PaymentAttempt { /// Payment id for the payment attempt pub payment_id: id_type::GlobalPaymentId, @@ -259,12 +268,11 @@ pub struct PaymentAttempt { pub connector_metadata: Option, pub payment_experience: Option, /// The insensitive data of the payment method data is stored here - // TODO: evaluate what details should be stored here. Use a domain type instead of serde_json::Value pub payment_method_data: Option, /// The result of the routing algorithm. /// This will store the list of connectors and other related information that was used to route the payment. // TODO: change this to type instead of serde_json::Value - pub routing_result: Option, + pub routing_result: Option, pub preprocessing_step_id: Option, /// Number of captures that have happened for the payment attempt pub multiple_capture_count: Option, @@ -306,8 +314,8 @@ pub struct PaymentAttempt { /// A reference to the payment at connector side. This is returned by the connector pub external_reference_id: Option, /// The billing address for the payment method - // TODO: use a type here instead of value - pub payment_method_billing_address: common_utils::crypto::OptionalEncryptableValue, + #[encrypt(ty = Value)] + pub payment_method_billing_address: Option>, /// The global identifier for the payment attempt pub id: id_type::GlobalAttemptId, /// The connector mandate details which are stored temporarily @@ -364,6 +372,7 @@ impl PaymentAttempt { cell_id: id_type::CellId, storage_scheme: storage_enums::MerchantStorageScheme, request: &api_models::payments::PaymentsConfirmIntentRequest, + encrypted_data: DecryptedPaymentAttempt, ) -> CustomResult { let id = id_type::GlobalAttemptId::generate(&cell_id); let intent_amount_details = payment_intent.amount_details.clone(); @@ -1755,13 +1764,39 @@ impl behaviour::Conversion for PaymentAttempt { where Self: Sized, { - use crate::type_encryption; - async { let connector_payment_id = storage_model .get_optional_connector_transaction_id() .cloned(); + let decrypted_data = crypto_operation( + state, + common_utils::type_name!(Self::DstType), + CryptoOperation::BatchDecrypt(EncryptedPaymentAttempt::to_encryptable( + EncryptedPaymentAttempt { + payment_method_billing_address: storage_model + .payment_method_billing_address, + }, + )), + key_manager_identifier, + key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation())?; + + let decrypted_data = EncryptedPaymentAttempt::from_encryptable(decrypted_data) + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Invalid batch operation data")?; + + let payment_method_billing_address = decrypted_data + .payment_method_billing_address + .map(|billing| { + billing.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Error while deserializing Address")?; + let amount_details = AttemptAmountDetails { net_amount: storage_model.net_amount, tax_on_surcharge: storage_model.tax_on_surcharge, @@ -1772,18 +1807,6 @@ impl behaviour::Conversion for PaymentAttempt { amount_to_capture: storage_model.amount_to_capture, }; - let inner_decrypt = |inner| async { - type_encryption::crypto_operation( - state, - common_utils::type_name!(Self::DstType), - type_encryption::CryptoOperation::DecryptOptional(inner), - key_manager_identifier.clone(), - key.peek(), - ) - .await - .and_then(|val| val.try_into_optionaloperation()) - }; - let error = storage_model .error_code .zip(storage_model.error_message) @@ -1838,10 +1861,7 @@ impl behaviour::Conversion for PaymentAttempt { authentication_applied: storage_model.authentication_applied, external_reference_id: storage_model.external_reference_id, connector: storage_model.connector, - payment_method_billing_address: inner_decrypt( - storage_model.payment_method_billing_address, - ) - .await?, + payment_method_billing_address, connector_mandate_detail: storage_model.connector_mandate_detail, }) } diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 0a03fc741f0..5965bdc8850 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -4,8 +4,10 @@ use api_models::{ payments::{ExtendedCardInfo, GetAddressFromPaymentMethodData, PaymentsConfirmIntentRequest}, }; use async_trait::async_trait; +use common_utils::{ext_traits::Encode, types::keymanager::ToEncryptable}; use error_stack::ResultExt; use hyperswitch_domain_models::payments::PaymentConfirmData; +use masking::PeekInterface; use router_env::{instrument, tracing}; use tracing_futures::Instrument; @@ -26,7 +28,7 @@ use crate::{ types::{ self, api::{self, ConnectorCallType, PaymentIdTypeExt}, - domain::{self}, + domain::{self, types as domain_types}, storage::{self, enums as storage_enums}, }, utils::{self, OptionExt}, @@ -176,12 +178,36 @@ impl GetTracker, PaymentsConfirmIntent let cell_id = state.conf.cell_information.id.clone(); + let batch_encrypted_data = domain_types::crypto_operation( + key_manager_state, + common_utils::type_name!(hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::to_encryptable( + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt { + payment_method_billing_address: request.payment_method_data.billing.as_ref().map(|address| address.clone().encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode payment_method_billing address")?.map(masking::Secret::new), + }, + ), + ), + common_utils::types::keymanager::Identifier::Merchant(merchant_account.get_id().to_owned()), + key_store.key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details".to_string())?; + + let encrypted_data = + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::from_encryptable(batch_encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details")?; + let payment_attempt_domain_model = hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt::create_domain_model( &payment_intent, cell_id, storage_scheme, - request + request, + encrypted_data ) .await?; diff --git a/crates/router/src/core/payments/operations/payment_create_intent.rs b/crates/router/src/core/payments/operations/payment_create_intent.rs index b46992a6aed..bf5b4fb80c9 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -4,9 +4,11 @@ use api_models::{enums::FrmSuggestion, payments::PaymentsCreateIntentRequest}; use async_trait::async_trait; use common_utils::{ errors::CustomResult, - ext_traits::{AsyncExt, ValueExt}, + ext_traits::{AsyncExt, Encode, ValueExt}, + types::keymanager::ToEncryptable, }; use error_stack::ResultExt; +use masking::PeekInterface; use router_env::{instrument, tracing}; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; @@ -18,7 +20,8 @@ use crate::{ routes::{app::ReqState, SessionState}, services, types::{ - api, domain, + api, + domain::{self, types as domain_types}, storage::{self, enums}, }, }; @@ -100,51 +103,39 @@ impl GetTracker, PaymentsCrea let key_manager_state = &state.into(); let storage_scheme = merchant_account.storage_scheme; - // Derivation of directly supplied Billing Address data in our Payment Create Request - // Encrypting our Billing Address Details to be stored in Payment Intent - let billing_address = request - .billing - .clone() - .async_map(|billing_details| { - create_encrypted_data(key_manager_state, key_store, billing_details) - }) - .await - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt billing details")? - .map(|encrypted_value| { - encrypted_value.deserialize_inner_value(|value| value.parse_value("Address")) - }) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize decrypted value to Address")?; - // Derivation of directly supplied Shipping Address data in our Payment Create Request - // Encrypting our Shipping Address Details to be stored in Payment Intent - let shipping_address = request - .shipping - .clone() - .async_map(|shipping_details| { - create_encrypted_data(key_manager_state, key_store, shipping_details) - }) - .await - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt shipping details")? - .map(|encrypted_value| { - encrypted_value.deserialize_inner_value(|value| value.parse_value("Address")) - }) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to deserialize decrypted value to Address")?; + let batch_encrypted_data = domain_types::crypto_operation( + key_manager_state, + common_utils::type_name!(hyperswitch_domain_models::payments::PaymentIntent), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent::to_encryptable( + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent { + shipping_address: request.shipping.clone().map(|address| address.encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode shipping address")?.map(masking::Secret::new), + billing_address: request.billing.clone().map(|address| address.encode_to_value()).transpose().change_context(errors::ApiErrorResponse::InternalServerError).attach_printable("Failed to encode billing address")?.map(masking::Secret::new), + customer_details: None, + }, + ), + ), + common_utils::types::keymanager::Identifier::Merchant(merchant_account.get_id().to_owned()), + key_store.key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details".to_string())?; + + let encrypted_data = + hyperswitch_domain_models::payments::FromRequestEncryptablePaymentIntent::from_encryptable(batch_encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details")?; + let payment_intent_domain = hyperswitch_domain_models::payments::PaymentIntent::create_domain_model_from_request( payment_id, merchant_account, profile, request.clone(), - billing_address, - shipping_address, + encrypted_data, ) .await?; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index d9bd374ef1a..32697508101 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -947,11 +947,13 @@ where billing: payment_intent .billing_address .clone() - .map(|billing| billing.into_inner().expose()), + .map(|billing| billing.into_inner()) + .map(From::from), shipping: payment_intent .shipping_address .clone() - .map(|shipping| shipping.into_inner().expose()), + .map(|shipping| shipping.into_inner()) + .map(From::from), customer_id: payment_intent.customer_id.clone(), customer_present: payment_intent.customer_present.clone(), description: payment_intent.description.clone(), diff --git a/crates/router_derive/src/macros/to_encryptable.rs b/crates/router_derive/src/macros/to_encryptable.rs index dfcfb72169b..561c3a72371 100644 --- a/crates/router_derive/src/macros/to_encryptable.rs +++ b/crates/router_derive/src/macros/to_encryptable.rs @@ -242,13 +242,17 @@ fn generate_to_encryptable( let inner_types = get_field_and_inner_types(&fields); - let inner_type = inner_types.first().map(|(_, ty)| ty).ok_or_else(|| { + let inner_type = inner_types.first().ok_or_else(|| { syn::Error::new( proc_macro2::Span::call_site(), "Please use the macro with attribute #[encrypt] on the fields you want to encrypt", ) })?; + let provided_ty = get_encryption_ty_meta(&inner_type.0) + .map(|ty| ty.value.clone()) + .unwrap_or(inner_type.1.clone()); + let structs = struct_types.iter().map(|(prefix, struct_type)| { let name = format_ident!("{}{}", prefix, struct_name); let temp_fields = struct_type.generate_struct_fields(&inner_types); @@ -275,15 +279,15 @@ fn generate_to_encryptable( let decrypted_name = format_ident!("Decrypted{}", struct_name); ( quote! { #decrypted_name }, - quote! { Secret<#inner_type> }, - quote! { Secret<#inner_type> }, + quote! { Secret<#provided_ty> }, + quote! { Secret<#provided_ty> }, ) } StructType::Encrypted => { let decrypted_name = format_ident!("Decrypted{}", struct_name); ( quote! { #decrypted_name }, - quote! { Secret<#inner_type> }, + quote! { Secret<#provided_ty> }, quote! { Encryption }, ) } @@ -291,8 +295,8 @@ fn generate_to_encryptable( let decrypted_update_name = format_ident!("DecryptedUpdate{}", struct_name); ( quote! { #decrypted_update_name }, - quote! { Secret<#inner_type> }, - quote! { Secret<#inner_type> }, + quote! { Secret<#provided_ty> }, + quote! { Secret<#provided_ty> }, ) } //Unreachable statement From c9df7b0557889c88ea20392dfe56bf651e22c9a7 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:47:58 +0530 Subject: [PATCH 14/51] refactor(tenant): use tenant id type (#6643) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/common_utils/src/id_type.rs | 2 + .../common_utils/src/id_type/organization.rs | 2 +- crates/common_utils/src/id_type/tenant.rs | 22 ++++++++++ crates/common_utils/src/types/theme.rs | 12 +++--- crates/diesel_models/src/user/theme.rs | 4 +- crates/diesel_models/src/user_role.rs | 4 +- crates/drainer/src/handler.rs | 8 +++- crates/drainer/src/health_check.rs | 4 +- crates/drainer/src/lib.rs | 6 +-- crates/drainer/src/settings.rs | 16 +++---- crates/router/src/configs/settings.rs | 25 ++++++----- .../src/core/payment_methods/transformers.rs | 35 +++++++++++----- crates/router/src/core/routing/helpers.rs | 5 ++- crates/router/src/core/user.rs | 8 +++- crates/router/src/db/events.rs | 5 ++- .../src/db/merchant_connector_account.rs | 11 ++++- crates/router/src/db/merchant_key_store.rs | 5 ++- crates/router/src/routes/app.rs | 15 ++++--- crates/router/src/services/api.rs | 42 ++++++++++++------- crates/router/src/services/authentication.rs | 16 +++---- crates/router/src/services/authorization.rs | 10 +++-- crates/router/src/types/domain/user.rs | 8 ++-- crates/router/src/utils/user.rs | 2 +- crates/router/tests/cache.rs | 5 ++- crates/router/tests/connectors/aci.rs | 20 +++++++-- crates/router/tests/connectors/utils.rs | 30 ++++++++++--- crates/router/tests/payments.rs | 10 ++++- crates/router/tests/payments2.rs | 10 ++++- crates/router/tests/services.rs | 10 ++++- crates/scheduler/src/consumer.rs | 6 +-- crates/scheduler/src/producer.rs | 6 +-- crates/scheduler/src/scheduler.rs | 6 +-- 32 files changed, 252 insertions(+), 118 deletions(-) create mode 100644 crates/common_utils/src/id_type/tenant.rs diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 3d57a72376e..a8085564145 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -12,6 +12,7 @@ mod payment; mod profile; mod refunds; mod routing; +mod tenant; #[cfg(feature = "v2")] mod global_id; @@ -40,6 +41,7 @@ pub use profile::ProfileId; pub use refunds::RefundReferenceId; pub use routing::RoutingId; use serde::{Deserialize, Serialize}; +pub use tenant::TenantId; use thiserror::Error; use crate::{fp_utils::when, generate_id_with_default_len}; diff --git a/crates/common_utils/src/id_type/organization.rs b/crates/common_utils/src/id_type/organization.rs index a83f35db1d8..2097fbb2450 100644 --- a/crates/common_utils/src/id_type/organization.rs +++ b/crates/common_utils/src/id_type/organization.rs @@ -18,7 +18,7 @@ crate::impl_to_sql_from_sql_id_type!(OrganizationId); impl OrganizationId { /// Get an organization id from String - pub fn wrap(org_id: String) -> CustomResult { + pub fn try_from_string(org_id: String) -> CustomResult { Self::try_from(std::borrow::Cow::from(org_id)) } } diff --git a/crates/common_utils/src/id_type/tenant.rs b/crates/common_utils/src/id_type/tenant.rs new file mode 100644 index 00000000000..953bf82287a --- /dev/null +++ b/crates/common_utils/src/id_type/tenant.rs @@ -0,0 +1,22 @@ +use crate::errors::{CustomResult, ValidationError}; + +crate::id_type!( + TenantId, + "A type for tenant_id that can be used for unique identifier for a tenant" +); +crate::impl_id_type_methods!(TenantId, "tenant_id"); + +// This is to display the `TenantId` as TenantId(abcd) +crate::impl_debug_id_type!(TenantId); +crate::impl_try_from_cow_str_id_type!(TenantId, "tenant_id"); + +crate::impl_serializable_secret_id_type!(TenantId); +crate::impl_queryable_id_type!(TenantId); +crate::impl_to_sql_from_sql_id_type!(TenantId); + +impl TenantId { + /// Get tenant id from String + pub fn try_from_string(tenant_id: String) -> CustomResult { + Self::try_from(std::borrow::Cow::from(tenant_id)) + } +} diff --git a/crates/common_utils/src/types/theme.rs b/crates/common_utils/src/types/theme.rs index 03b4cf23a6c..9ad9206acce 100644 --- a/crates/common_utils/src/types/theme.rs +++ b/crates/common_utils/src/types/theme.rs @@ -12,15 +12,15 @@ pub enum ThemeLineage { // }, /// Org lineage variant Organization { - /// tenant_id: String - tenant_id: String, + /// tenant_id: TenantId + tenant_id: id_type::TenantId, /// org_id: OrganizationId org_id: id_type::OrganizationId, }, /// Merchant lineage variant Merchant { - /// tenant_id: String - tenant_id: String, + /// tenant_id: TenantId + tenant_id: id_type::TenantId, /// org_id: OrganizationId org_id: id_type::OrganizationId, /// merchant_id: MerchantId @@ -28,8 +28,8 @@ pub enum ThemeLineage { }, /// Profile lineage variant Profile { - /// tenant_id: String - tenant_id: String, + /// tenant_id: TenantId + tenant_id: id_type::TenantId, /// org_id: OrganizationId org_id: id_type::OrganizationId, /// merchant_id: MerchantId diff --git a/crates/diesel_models/src/user/theme.rs b/crates/diesel_models/src/user/theme.rs index 2f8152e419c..9841e21443c 100644 --- a/crates/diesel_models/src/user/theme.rs +++ b/crates/diesel_models/src/user/theme.rs @@ -9,7 +9,7 @@ use crate::schema::themes; #[diesel(table_name = themes, primary_key(theme_id), check_for_backend(diesel::pg::Pg))] pub struct Theme { pub theme_id: String, - pub tenant_id: String, + pub tenant_id: id_type::TenantId, pub org_id: Option, pub merchant_id: Option, pub profile_id: Option, @@ -23,7 +23,7 @@ pub struct Theme { #[diesel(table_name = themes)] pub struct ThemeNew { pub theme_id: String, - pub tenant_id: String, + pub tenant_id: id_type::TenantId, pub org_id: Option, pub merchant_id: Option, pub profile_id: Option, diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs index ceddbfd61e4..04f3264b45e 100644 --- a/crates/diesel_models/src/user_role.rs +++ b/crates/diesel_models/src/user_role.rs @@ -24,7 +24,7 @@ pub struct UserRole { pub entity_id: Option, pub entity_type: Option, pub version: enums::UserRoleVersion, - pub tenant_id: String, + pub tenant_id: id_type::TenantId, } impl UserRole { @@ -88,7 +88,7 @@ pub struct UserRoleNew { pub entity_id: Option, pub entity_type: Option, pub version: enums::UserRoleVersion, - pub tenant_id: String, + pub tenant_id: id_type::TenantId, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] diff --git a/crates/drainer/src/handler.rs b/crates/drainer/src/handler.rs index d8a8bff5afc..d0c26195453 100644 --- a/crates/drainer/src/handler.rs +++ b/crates/drainer/src/handler.rs @@ -3,6 +3,7 @@ use std::{ sync::{atomic, Arc}, }; +use common_utils::id_type; use router_env::tracing::Instrument; use tokio::{ sync::{mpsc, oneshot}, @@ -34,12 +35,15 @@ pub struct HandlerInner { loop_interval: Duration, active_tasks: Arc, conf: DrainerSettings, - stores: HashMap>, + stores: HashMap>, running: Arc, } impl Handler { - pub fn from_conf(conf: DrainerSettings, stores: HashMap>) -> Self { + pub fn from_conf( + conf: DrainerSettings, + stores: HashMap>, + ) -> Self { let shutdown_interval = Duration::from_millis(conf.shutdown_interval.into()); let loop_interval = Duration::from_millis(conf.loop_interval.into()); diff --git a/crates/drainer/src/health_check.rs b/crates/drainer/src/health_check.rs index 48d5f311905..2ca2c1cc79c 100644 --- a/crates/drainer/src/health_check.rs +++ b/crates/drainer/src/health_check.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use actix_web::{web, Scope}; use async_bb8_diesel::{AsyncConnection, AsyncRunQueryDsl}; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, id_type}; use diesel_models::{Config, ConfigNew}; use error_stack::ResultExt; use router_env::{instrument, logger, tracing}; @@ -20,7 +20,7 @@ pub const TEST_STREAM_DATA: &[(&str, &str)] = &[("data", "sample_data")]; pub struct Health; impl Health { - pub fn server(conf: Settings, stores: HashMap>) -> Scope { + pub fn server(conf: Settings, stores: HashMap>) -> Scope { web::scope("health") .app_data(web::Data::new(conf)) .app_data(web::Data::new(stores)) diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 5b67640663c..6eb8c505e15 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -14,7 +14,7 @@ use std::{collections::HashMap, sync::Arc}; mod secrets_transformers; use actix_web::dev::Server; -use common_utils::signals::get_allowed_signals; +use common_utils::{id_type, signals::get_allowed_signals}; use diesel_models::kv; use error_stack::ResultExt; use hyperswitch_interfaces::secrets_interface::secret_state::RawSecret; @@ -31,7 +31,7 @@ use crate::{ }; pub async fn start_drainer( - stores: HashMap>, + stores: HashMap>, conf: DrainerSettings, ) -> errors::DrainerResult<()> { let drainer_handler = handler::Handler::from_conf(conf, stores); @@ -62,7 +62,7 @@ pub async fn start_drainer( pub async fn start_web_server( conf: Settings, - stores: HashMap>, + stores: HashMap>, ) -> Result { let server = conf.server.clone(); let web_server = actix_web::HttpServer::new(move || { diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index 5b391b492e0..9b6c88b3466 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc}; -use common_utils::{ext_traits::ConfigExt, DbConnectionParams}; +use common_utils::{ext_traits::ConfigExt, id_type, DbConnectionParams}; use config::{Environment, File}; use external_services::managers::{ encryption_management::EncryptionManagementConfig, secrets_management::SecretsManagementConfig, @@ -122,23 +122,23 @@ pub struct Multitenancy { pub tenants: TenantConfig, } impl Multitenancy { - pub fn get_tenants(&self) -> &HashMap { + pub fn get_tenants(&self) -> &HashMap { &self.tenants.0 } - pub fn get_tenant_ids(&self) -> Vec { + pub fn get_tenant_ids(&self) -> Vec { self.tenants .0 .values() .map(|tenant| tenant.tenant_id.clone()) .collect() } - pub fn get_tenant(&self, tenant_id: &str) -> Option<&Tenant> { + pub fn get_tenant(&self, tenant_id: &id_type::TenantId) -> Option<&Tenant> { self.tenants.0.get(tenant_id) } } #[derive(Debug, Clone, Default)] -pub struct TenantConfig(pub HashMap); +pub struct TenantConfig(pub HashMap); impl<'de> Deserialize<'de> for TenantConfig { fn deserialize>(deserializer: D) -> Result { @@ -150,7 +150,7 @@ impl<'de> Deserialize<'de> for TenantConfig { clickhouse_database: String, } - let hashmap = >::deserialize(deserializer)?; + let hashmap = >::deserialize(deserializer)?; Ok(Self( hashmap @@ -172,9 +172,9 @@ impl<'de> Deserialize<'de> for TenantConfig { } } -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Deserialize, Clone)] pub struct Tenant { - pub tenant_id: String, + pub tenant_id: id_type::TenantId, pub base_url: String, pub schema: String, pub redis_key_prefix: String, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 76b58f5b67b..7b212ec6d1d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -6,7 +6,7 @@ use std::{ #[cfg(feature = "olap")] use analytics::{opensearch::OpenSearchConfig, ReportConfig}; use api_models::{enums, payment_methods::RequiredFieldInfo}; -use common_utils::ext_traits::ConfigExt; +use common_utils::{ext_traits::ConfigExt, id_type}; use config::{Environment, File}; use error_stack::ResultExt; #[cfg(feature = "email")] @@ -138,17 +138,17 @@ pub struct Multitenancy { } impl Multitenancy { - pub fn get_tenants(&self) -> &HashMap { + pub fn get_tenants(&self) -> &HashMap { &self.tenants.0 } - pub fn get_tenant_ids(&self) -> Vec { + pub fn get_tenant_ids(&self) -> Vec { self.tenants .0 .values() .map(|tenant| tenant.tenant_id.clone()) .collect() } - pub fn get_tenant(&self, tenant_id: &str) -> Option<&Tenant> { + pub fn get_tenant(&self, tenant_id: &id_type::TenantId) -> Option<&Tenant> { self.tenants.0.get(tenant_id) } } @@ -159,11 +159,11 @@ pub struct DecisionConfig { } #[derive(Debug, Clone, Default)] -pub struct TenantConfig(pub HashMap); +pub struct TenantConfig(pub HashMap); -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct Tenant { - pub tenant_id: String, + pub tenant_id: id_type::TenantId, pub base_url: String, pub schema: String, pub redis_key_prefix: String, @@ -743,8 +743,7 @@ pub struct LockerBasedRecipientConnectorList { #[derive(Debug, Deserialize, Clone, Default)] pub struct ConnectorRequestReferenceIdConfig { - pub merchant_ids_send_payment_id_as_connector_request_id: - HashSet, + pub merchant_ids_send_payment_id_as_connector_request_id: HashSet, } #[derive(Debug, Deserialize, Clone, Default)] @@ -970,7 +969,7 @@ pub struct ServerTls { #[cfg(feature = "v2")] #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct CellInformation { - pub id: common_utils::id_type::CellId, + pub id: id_type::CellId, } #[cfg(feature = "v2")] @@ -981,8 +980,8 @@ impl Default for CellInformation { // around the time of deserializing application settings. // And a panic at application startup is considered acceptable. #[allow(clippy::expect_used)] - let cell_id = common_utils::id_type::CellId::from_string("defid") - .expect("Failed to create a default for Cell Id"); + let cell_id = + id_type::CellId::from_string("defid").expect("Failed to create a default for Cell Id"); Self { id: cell_id } } } @@ -1120,7 +1119,7 @@ impl<'de> Deserialize<'de> for TenantConfig { clickhouse_database: String, } - let hashmap = >::deserialize(deserializer)?; + let hashmap = >::deserialize(deserializer)?; Ok(Self( hashmap diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index c0f54a30f3f..c3fbfd8afbf 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -389,7 +389,7 @@ pub async fn mk_add_locker_request_hs( locker: &settings::Locker, payload: &StoreLockerReq, locker_choice: api_enums::LockerChoice, - tenant_id: String, + tenant_id: id_type::TenantId, request_id: Option, ) -> CustomResult { let payload = payload @@ -409,7 +409,10 @@ pub async fn mk_add_locker_request_hs( url.push_str("/cards/add"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.add_header(headers::X_TENANT_ID, tenant_id.into()); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); if let Some(req_id) = request_id { request.add_header( headers::X_REQUEST_ID, @@ -584,7 +587,7 @@ pub async fn mk_get_card_request_hs( merchant_id: &id_type::MerchantId, card_reference: &str, locker_choice: Option, - tenant_id: String, + tenant_id: id_type::TenantId, request_id: Option, ) -> CustomResult { let merchant_customer_id = customer_id.to_owned(); @@ -612,7 +615,10 @@ pub async fn mk_get_card_request_hs( url.push_str("/cards/retrieve"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.add_header(headers::X_TENANT_ID, tenant_id.into()); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); if let Some(req_id) = request_id { request.add_header( headers::X_REQUEST_ID, @@ -665,7 +671,7 @@ pub async fn mk_delete_card_request_hs( customer_id: &id_type::CustomerId, merchant_id: &id_type::MerchantId, card_reference: &str, - tenant_id: String, + tenant_id: id_type::TenantId, request_id: Option, ) -> CustomResult { let merchant_customer_id = customer_id.to_owned(); @@ -691,7 +697,10 @@ pub async fn mk_delete_card_request_hs( url.push_str("/cards/delete"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.add_header(headers::X_TENANT_ID, tenant_id.into()); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); if let Some(req_id) = request_id { request.add_header( headers::X_REQUEST_ID, @@ -711,7 +720,7 @@ pub async fn mk_delete_card_request_hs_by_id( id: &String, merchant_id: &id_type::MerchantId, card_reference: &str, - tenant_id: String, + tenant_id: id_type::TenantId, request_id: Option, ) -> CustomResult { let merchant_customer_id = id.to_owned(); @@ -737,7 +746,10 @@ pub async fn mk_delete_card_request_hs_by_id( url.push_str("/cards/delete"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.add_header(headers::X_TENANT_ID, tenant_id.into()); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); if let Some(req_id) = request_id { request.add_header( headers::X_REQUEST_ID, @@ -832,7 +844,7 @@ pub fn mk_crud_locker_request( locker: &settings::Locker, path: &str, req: api::TokenizePayloadEncrypted, - tenant_id: String, + tenant_id: id_type::TenantId, request_id: Option, ) -> CustomResult { let mut url = locker.basilisk_host.to_owned(); @@ -840,7 +852,10 @@ pub fn mk_crud_locker_request( let mut request = services::Request::new(services::Method::Post, &url); request.add_default_headers(); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.add_header(headers::X_TENANT_ID, tenant_id.into()); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); if let Some(req_id) = request_id { request.add_header( headers::X_REQUEST_ID, diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index aef89ea8ed9..264328796c8 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -751,7 +751,10 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( &metrics::CONTEXT, 1, &add_attributes([ - ("tenant", state.tenant.tenant_id.clone()), + ( + "tenant", + state.tenant.tenant_id.get_string_repr().to_owned(), + ), ( "merchant_profile_id", format!( diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index eedd9ac5672..f01f6c5d749 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1120,11 +1120,15 @@ pub async fn create_internal_user( } })?; - let default_tenant_id = common_utils::consts::DEFAULT_TENANT.to_string(); + let default_tenant_id = common_utils::id_type::TenantId::try_from_string( + common_utils::consts::DEFAULT_TENANT.to_owned(), + ) + .change_context(UserErrors::InternalServerError) + .attach_printable("Unable to parse default tenant id")?; if state.tenant.tenant_id != default_tenant_id { return Err(UserErrors::ForbiddenTenantId) - .attach_printable("Operation allowed only for the default tenant."); + .attach_printable("Operation allowed only for the default tenant"); } let internal_merchant_id = common_utils::id_type::MerchantId::get_internal_user_merchant_id( diff --git a/crates/router/src/db/events.rs b/crates/router/src/db/events.rs index 651c2ece610..6bb7de1b7d9 100644 --- a/crates/router/src/db/events.rs +++ b/crates/router/src/db/events.rs @@ -732,7 +732,10 @@ mod tests { )) .await; let state = &Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let merchant_id = common_utils::id_type::MerchantId::try_from(std::borrow::Cow::from("merchant_1")) diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index be2c25d4767..687f6e8fea2 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -1502,8 +1502,12 @@ mod merchant_connector_account_cache_tests { Box::new(services::MockApiClient), )) .await; + let state = &Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); #[allow(clippy::expect_used)] let db = MockDb::new(&redis_interface::RedisSettings::default()) @@ -1685,7 +1689,10 @@ mod merchant_connector_account_cache_tests { )) .await; let state = &Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); #[allow(clippy::expect_used)] let db = MockDb::new(&redis_interface::RedisSettings::default()) diff --git a/crates/router/src/db/merchant_key_store.rs b/crates/router/src/db/merchant_key_store.rs index 65a5515a391..9f12ec8e8fd 100644 --- a/crates/router/src/db/merchant_key_store.rs +++ b/crates/router/src/db/merchant_key_store.rs @@ -348,7 +348,10 @@ mod tests { )) .await; let state = &Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); #[allow(clippy::expect_used)] let mock_db = MockDb::new(&redis_interface::RedisSettings::default()) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index baa2ba4ae15..1584cfae2b9 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -7,6 +7,7 @@ use api_models::routing::RoutingRetrieveQuery; use common_enums::TransactionType; #[cfg(feature = "partial-auth")] use common_utils::crypto::Blake3; +use common_utils::id_type; #[cfg(feature = "email")] use external_services::email::{ no_email::NoEmailClient, ses::AwsSes, smtp::SmtpServer, EmailClientConfigs, EmailService, @@ -193,14 +194,14 @@ impl SessionStateInfo for SessionState { pub struct AppState { pub flow_name: String, pub global_store: Box, - pub stores: HashMap>, + pub stores: HashMap>, pub conf: Arc>, pub event_handler: EventsHandler, #[cfg(feature = "email")] pub email_client: Arc>, pub api_client: Box, #[cfg(feature = "olap")] - pub pools: HashMap, + pub pools: HashMap, #[cfg(feature = "olap")] pub opensearch_client: Arc, pub request_id: Option, @@ -209,7 +210,7 @@ pub struct AppState { pub grpc_client: Arc, } impl scheduler::SchedulerAppState for AppState { - fn get_tenants(&self) -> Vec { + fn get_tenants(&self) -> Vec { self.conf.multitenancy.get_tenant_ids() } } @@ -328,7 +329,7 @@ impl AppState { ); #[cfg(feature = "olap")] - let mut pools: HashMap = HashMap::new(); + let mut pools: HashMap = HashMap::new(); let mut stores = HashMap::new(); #[allow(clippy::expect_used)] let cache_store = get_cache_store(&conf.clone(), shut_down_signal, testable) @@ -443,7 +444,11 @@ impl AppState { .await } - pub fn get_session_state(self: Arc, tenant: &str, err: F) -> Result + pub fn get_session_state( + self: Arc, + tenant: &id_type::TenantId, + err: F, + ) -> Result where F: FnOnce() -> E + Copy, { diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index bdd650389a7..9416ff175a8 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -68,7 +68,7 @@ use crate::{ api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, connector_api_logs::ConnectorEvent, }, - logger, + headers, logger, routes::{ app::{AppStateInfo, ReqState, SessionStateInfo}, metrics, AppState, SessionState, @@ -722,33 +722,44 @@ where let mut event_type = payload.get_api_event_type(); let tenant_id = if !state.conf.multitenancy.enabled { - DEFAULT_TENANT.to_string() + common_utils::id_type::TenantId::try_from_string(DEFAULT_TENANT.to_owned()) + .attach_printable("Unable to get default tenant id") + .change_context(errors::ApiErrorResponse::InternalServerError.switch())? } else { let request_tenant_id = incoming_request_header .get(TENANT_HEADER) .and_then(|value| value.to_str().ok()) - .ok_or_else(|| errors::ApiErrorResponse::MissingTenantId.switch())?; + .ok_or_else(|| errors::ApiErrorResponse::MissingTenantId.switch()) + .and_then(|header_value| { + common_utils::id_type::TenantId::try_from_string(header_value.to_string()).map_err( + |_| { + errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_TENANT_ID), + } + .switch() + }, + ) + })?; state .conf .multitenancy - .get_tenant(request_tenant_id) + .get_tenant(&request_tenant_id) .map(|tenant| tenant.tenant_id.clone()) .ok_or( errors::ApiErrorResponse::InvalidTenant { - tenant_id: request_tenant_id.to_string(), + tenant_id: request_tenant_id.get_string_repr().to_string(), } .switch(), )? }; - let mut session_state = - Arc::new(app_state.clone()).get_session_state(tenant_id.as_str(), || { - errors::ApiErrorResponse::InvalidTenant { - tenant_id: tenant_id.clone(), - } - .switch() - })?; + let mut session_state = Arc::new(app_state.clone()).get_session_state(&tenant_id, || { + errors::ApiErrorResponse::InvalidTenant { + tenant_id: tenant_id.get_string_repr().to_string(), + } + .switch() + })?; session_state.add_request_id(request_id); let mut request_state = session_state.get_req_state(); @@ -757,9 +768,10 @@ where .event_context .record_info(("flow".to_string(), flow.to_string())); - request_state - .event_context - .record_info(("tenant_id".to_string(), tenant_id.to_string())); + request_state.event_context.record_info(( + "tenant_id".to_string(), + tenant_id.get_string_repr().to_string(), + )); // Currently auth failures are not recorded as API events let (auth_out, auth_type) = api_auth diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 2541a5dc7d4..2f5f55b8434 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -185,7 +185,7 @@ pub struct UserFromSinglePurposeToken { pub user_id: String, pub origin: domain::Origin, pub path: Vec, - pub tenant_id: Option, + pub tenant_id: Option, } #[cfg(feature = "olap")] @@ -196,7 +196,7 @@ pub struct SinglePurposeToken { pub origin: domain::Origin, pub path: Vec, pub exp: u64, - pub tenant_id: Option, + pub tenant_id: Option, } #[cfg(feature = "olap")] @@ -207,7 +207,7 @@ impl SinglePurposeToken { origin: domain::Origin, settings: &Settings, path: Vec, - tenant_id: Option, + tenant_id: Option, ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::SINGLE_PURPOSE_TOKEN_TIME_IN_SECS); @@ -232,7 +232,7 @@ pub struct AuthToken { pub exp: u64, pub org_id: id_type::OrganizationId, pub profile_id: id_type::ProfileId, - pub tenant_id: Option, + pub tenant_id: Option, } #[cfg(feature = "olap")] @@ -244,7 +244,7 @@ impl AuthToken { settings: &Settings, org_id: id_type::OrganizationId, profile_id: id_type::ProfileId, - tenant_id: Option, + tenant_id: Option, ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(exp_duration)?.as_secs(); @@ -268,7 +268,7 @@ pub struct UserFromToken { pub role_id: String, pub org_id: id_type::OrganizationId, pub profile_id: id_type::ProfileId, - pub tenant_id: Option, + pub tenant_id: Option, } pub struct UserIdFromAuth { @@ -282,7 +282,7 @@ pub struct SinglePurposeOrLoginToken { pub role_id: Option, pub purpose: Option, pub exp: u64, - pub tenant_id: Option, + pub tenant_id: Option, } pub trait AuthInfo { @@ -1110,7 +1110,7 @@ impl<'a> HeaderMapStruct<'a> { self.get_mandatory_header_value_by_key(headers::X_ORGANIZATION_ID) .map(|val| val.to_owned()) .and_then(|organization_id| { - id_type::OrganizationId::wrap(organization_id).change_context( + id_type::OrganizationId::try_from_string(organization_id).change_context( errors::ApiErrorResponse::InvalidRequestData { message: format!("`{}` header is invalid", headers::X_ORGANIZATION_ID), }, diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs index 35a0b159ab2..87ff9f6abd5 100644 --- a/crates/router/src/services/authorization.rs +++ b/crates/router/src/services/authorization.rs @@ -112,12 +112,16 @@ pub fn check_permission( ) } -pub fn check_tenant(token_tenant_id: Option, header_tenant_id: &str) -> RouterResult<()> { +pub fn check_tenant( + token_tenant_id: Option, + header_tenant_id: &id_type::TenantId, +) -> RouterResult<()> { if let Some(tenant_id) = token_tenant_id { - if tenant_id != header_tenant_id { + if tenant_id != *header_tenant_id { return Err(ApiErrorResponse::InvalidJwtToken).attach_printable(format!( "Token tenant ID: '{}' does not match Header tenant ID: '{}'", - tenant_id, header_tenant_id + tenant_id.get_string_repr().to_owned(), + header_tenant_id.get_string_repr().to_owned() )); } } diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 4cb69e68ed8..6d0d2a4ea07 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -1106,20 +1106,20 @@ pub struct NoLevel; #[derive(Clone)] pub struct OrganizationLevel { - pub tenant_id: String, + pub tenant_id: id_type::TenantId, pub org_id: id_type::OrganizationId, } #[derive(Clone)] pub struct MerchantLevel { - pub tenant_id: String, + pub tenant_id: id_type::TenantId, pub org_id: id_type::OrganizationId, pub merchant_id: id_type::MerchantId, } #[derive(Clone)] pub struct ProfileLevel { - pub tenant_id: String, + pub tenant_id: id_type::TenantId, pub org_id: id_type::OrganizationId, pub merchant_id: id_type::MerchantId, pub profile_id: id_type::ProfileId, @@ -1156,7 +1156,7 @@ impl NewUserRole { } pub struct EntityInfo { - tenant_id: String, + tenant_id: id_type::TenantId, org_id: id_type::OrganizationId, merchant_id: Option, profile_id: Option, diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 8a9daefb287..f115a16c062 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -92,7 +92,7 @@ pub async fn generate_jwt_auth_token_with_attributes( org_id: id_type::OrganizationId, role_id: String, profile_id: id_type::ProfileId, - tenant_id: Option, + tenant_id: Option, ) -> UserResult> { let token = AuthToken::new_token( user_id, diff --git a/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index 8c3f34cd1e1..55b92b4aace 100644 --- a/crates/router/tests/cache.rs +++ b/crates/router/tests/cache.rs @@ -18,7 +18,10 @@ async fn invalidate_existing_cache_success() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let cache_key = "cacheKey".to_string(); let cache_key_value = "val".to_string(); diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 10c8a3dd012..92cba2eec3f 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -218,7 +218,10 @@ async fn payments_create_success() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); use router::connector::Aci; @@ -265,7 +268,10 @@ async fn payments_create_failure() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let connector = utils::construct_connector_data_old( Box::new(Aci::new()), @@ -328,7 +334,10 @@ async fn refund_for_successful_payments() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let connector_integration: services::BoxedPaymentConnectorIntegrationInterface< types::api::Authorize, @@ -398,7 +407,10 @@ async fn refunds_create_failure() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let connector_integration: services::BoxedRefundConnectorIntegrationInterface< types::api::Execute, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index a03148956ba..f8f71a98283 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -601,7 +601,10 @@ pub trait ConnectorActions: Connector { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let res = services::api::execute_connector_processing_step( &state, @@ -641,7 +644,10 @@ pub trait ConnectorActions: Connector { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let res = services::api::execute_connector_processing_step( &state, @@ -682,7 +688,10 @@ pub trait ConnectorActions: Connector { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let res = services::api::execute_connector_processing_step( &state, @@ -722,7 +731,10 @@ pub trait ConnectorActions: Connector { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let res = services::api::execute_connector_processing_step( &state, @@ -813,7 +825,10 @@ pub trait ConnectorActions: Connector { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let res = services::api::execute_connector_processing_step( &state, @@ -850,7 +865,10 @@ async fn call_connector< )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); services::api::execute_connector_processing_step( &state, diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index df4340a353e..68ca08c8bd3 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -295,7 +295,10 @@ async fn payments_create_core() { let merchant_id = id_type::MerchantId::try_from(Cow::from("juspay_merchant")).unwrap(); let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let key_manager_state = &(&state).into(); let key_store = state @@ -552,7 +555,10 @@ async fn payments_create_core_adyen_no_redirect() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let payment_id = diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index b5962d454fa..90fe3a1f847 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -56,7 +56,10 @@ async fn payments_create_core() { let merchant_id = id_type::MerchantId::try_from(Cow::from("juspay_merchant")).unwrap(); let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let key_manager_state = &(&state).into(); let key_store = state @@ -321,7 +324,10 @@ async fn payments_create_core_adyen_no_redirect() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let customer_id = format!("cust_{}", Uuid::new_v4()); diff --git a/crates/router/tests/services.rs b/crates/router/tests/services.rs index d907000cccd..c014370b24f 100644 --- a/crates/router/tests/services.rs +++ b/crates/router/tests/services.rs @@ -18,7 +18,10 @@ async fn get_redis_conn_failure() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); let _ = state.store.get_redis_conn().map(|conn| { @@ -46,7 +49,10 @@ async fn get_redis_conn_success() { )) .await; let state = Arc::new(app_state) - .get_session_state("public", || {}) + .get_session_state( + &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + || {}, + ) .unwrap(); // Act diff --git a/crates/scheduler/src/consumer.rs b/crates/scheduler/src/consumer.rs index 5791edd31b0..846f1137b12 100644 --- a/crates/scheduler/src/consumer.rs +++ b/crates/scheduler/src/consumer.rs @@ -7,7 +7,7 @@ use std::{ pub mod types; pub mod workflows; -use common_utils::{errors::CustomResult, signals::get_allowed_signals}; +use common_utils::{errors::CustomResult, id_type, signals::get_allowed_signals}; use diesel_models::enums; pub use diesel_models::{self, process_tracker as storage}; use error_stack::ResultExt; @@ -42,7 +42,7 @@ pub async fn start_consumer CustomResult<(), errors::ProcessTrackerError> where - F: Fn(&T, &str) -> CustomResult, + F: Fn(&T, &id_type::TenantId) -> CustomResult, { use std::time::Duration; @@ -88,7 +88,7 @@ where let start_time = std_time::Instant::now(); let tenants = state.get_tenants(); for tenant in tenants { - let session_state = app_state_to_session_state(state, tenant.as_str())?; + let session_state = app_state_to_session_state(state, &tenant)?; pt_utils::consumer_operation_handler( session_state.clone(), settings.clone(), diff --git a/crates/scheduler/src/producer.rs b/crates/scheduler/src/producer.rs index 6f710f55a34..b91434fcbb0 100644 --- a/crates/scheduler/src/producer.rs +++ b/crates/scheduler/src/producer.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, id_type}; use diesel_models::enums::ProcessTrackerStatus; use error_stack::{report, ResultExt}; use router_env::{ @@ -27,7 +27,7 @@ pub async fn start_producer( app_state_to_session_state: F, ) -> CustomResult<(), errors::ProcessTrackerError> where - F: Fn(&T, &str) -> CustomResult, + F: Fn(&T, &id_type::TenantId) -> CustomResult, T: SchedulerAppState, U: SchedulerSessionState, { @@ -69,7 +69,7 @@ where interval.tick().await; let tenants = state.get_tenants(); for tenant in tenants { - let session_state = app_state_to_session_state(state, tenant.as_str())?; + let session_state = app_state_to_session_state(state, &tenant)?; match run_producer_flow(&session_state, &scheduler_settings).await { Ok(_) => (), Err(error) => { diff --git a/crates/scheduler/src/scheduler.rs b/crates/scheduler/src/scheduler.rs index 39a45d02ba9..2685c6311ea 100644 --- a/crates/scheduler/src/scheduler.rs +++ b/crates/scheduler/src/scheduler.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, id_type}; use storage_impl::mock_db::MockDb; #[cfg(feature = "kv_store")] use storage_impl::KVRouterStore; @@ -52,7 +52,7 @@ impl SchedulerInterface for MockDb {} #[async_trait::async_trait] pub trait SchedulerAppState: Send + Sync + Clone { - fn get_tenants(&self) -> Vec; + fn get_tenants(&self) -> Vec; } #[async_trait::async_trait] pub trait SchedulerSessionState: Send + Sync + Clone { @@ -71,7 +71,7 @@ pub async fn start_process_tracker< app_state_to_session_state: F, ) -> CustomResult<(), errors::ProcessTrackerError> where - F: Fn(&T, &str) -> CustomResult, + F: Fn(&T, &id_type::TenantId) -> CustomResult, { match scheduler_flow { SchedulerFlow::Producer => { From acb30ef6d144eaf13b237b830d1ac534259932a3 Mon Sep 17 00:00:00 2001 From: Sidharth-Singh10 <70999945+Sidharth-Singh10@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:49:45 +0530 Subject: [PATCH 15/51] refactor(connector): add amount conversion framework to Riskified (#6359) --- crates/router/src/connector/riskified.rs | 41 ++++++- .../connector/riskified/transformers/api.rs | 106 +++++++++++++----- crates/router/src/types/api/fraud_check.rs | 2 +- 3 files changed, 114 insertions(+), 35 deletions(-) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index 0f8ebb46134..7d9feec717a 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -1,8 +1,10 @@ pub mod transformers; -use std::fmt::Debug; #[cfg(feature = "frm")] use base64::Engine; +use common_utils::types::{ + AmountConvertor, MinorUnit, StringMajorUnit, StringMajorUnitForConnector, +}; #[cfg(feature = "frm")] use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; #[cfg(feature = "frm")] @@ -14,6 +16,7 @@ use ring::hmac; #[cfg(feature = "frm")] use transformers as riskified; +use super::utils::convert_amount; #[cfg(feature = "frm")] use super::utils::{self as connector_utils, FrmTransactionRouterDataRequest}; use crate::{ @@ -35,10 +38,18 @@ use crate::{ utils::BytesExt, }; -#[derive(Debug, Clone)] -pub struct Riskified; +#[derive(Clone)] +pub struct Riskified { + amount_converter: &'static (dyn AmountConvertor + Sync), +} impl Riskified { + pub fn new() -> &'static Self { + &Self { + amount_converter: &StringMajorUnitForConnector, + } + } + #[cfg(feature = "frm")] pub fn generate_authorization_signature( &self, @@ -173,7 +184,17 @@ impl req: &frm_types::FrmCheckoutRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let req_obj = riskified::RiskifiedPaymentsCheckoutRequest::try_from(req)?; + let amount = convert_amount( + self.amount_converter, + MinorUnit::new(req.request.amount), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + )?; + let req_data = riskified::RiskifiedRouterData::from((amount, req)); + let req_obj = riskified::RiskifiedPaymentsCheckoutRequest::try_from(&req_data)?; Ok(RequestContent::Json(Box::new(req_obj))) } @@ -293,7 +314,17 @@ impl Ok(RequestContent::Json(Box::new(req_obj))) } _ => { - let req_obj = riskified::TransactionSuccessRequest::try_from(req)?; + let amount = convert_amount( + self.amount_converter, + MinorUnit::new(req.request.amount), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + )?; + let req_data = riskified::RiskifiedRouterData::from((amount, req)); + let req_obj = riskified::TransactionSuccessRequest::try_from(&req_data)?; Ok(RequestContent::Json(Box::new(req_obj))) } } diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index cfa183e9e83..c2da0193f99 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -1,5 +1,10 @@ use api_models::payments::AdditionalPaymentData; -use common_utils::{ext_traits::ValueExt, id_type, pii::Email}; +use common_utils::{ + ext_traits::ValueExt, + id_type, + pii::Email, + types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector}, +}; use error_stack::{self, ResultExt}; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -7,17 +12,37 @@ use time::PrimitiveDateTime; use crate::{ connector::utils::{ - AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckTransactionRequest, RouterData, + convert_amount, AddressDetailsData, FraudCheckCheckoutRequest, + FraudCheckTransactionRequest, RouterData, }, core::{errors, fraud_check::types as core_types}, types::{ - self, api, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + self, + api::{self, Fulfillment}, + fraud_check as frm_types, + storage::enums as storage_enums, ResponseId, ResponseRouterData, }, }; type Error = error_stack::Report; +pub struct RiskifiedRouterData { + pub amount: StringMajorUnit, + pub router_data: T, + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl From<(StringMajorUnit, T)> for RiskifiedRouterData { + fn from((amount, router_data): (StringMajorUnit, T)) -> Self { + Self { + amount, + router_data, + amount_converter: &StringMajorUnitForConnector, + } + } +} + #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct RiskifiedPaymentsCheckoutRequest { order: CheckoutRequest, @@ -35,7 +60,7 @@ pub struct CheckoutRequest { updated_at: PrimitiveDateTime, gateway: Option, browser_ip: Option, - total_price: i64, + total_price: StringMajorUnit, total_discounts: i64, cart_token: String, referring_site: String, @@ -60,13 +85,13 @@ pub struct PaymentDetails { #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct ShippingLines { - price: i64, + price: StringMajorUnit, title: Option, } #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct DiscountCodes { - amount: i64, + amount: StringMajorUnit, code: Option, } @@ -110,7 +135,7 @@ pub struct OrderAddress { #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct LineItem { - price: i64, + price: StringMajorUnit, quantity: i32, title: String, product_type: Option, @@ -132,9 +157,14 @@ pub struct RiskifiedMetadata { shipping_lines: Vec, } -impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutRequest { +impl TryFrom<&RiskifiedRouterData<&frm_types::FrmCheckoutRouterData>> + for RiskifiedPaymentsCheckoutRequest +{ type Error = Error; - fn try_from(payment_data: &frm_types::FrmCheckoutRouterData) -> Result { + fn try_from( + payment: &RiskifiedRouterData<&frm_types::FrmCheckoutRouterData>, + ) -> Result { + let payment_data = payment.router_data.clone(); let metadata: RiskifiedMetadata = payment_data .frm_metadata .clone() @@ -148,6 +178,33 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutReq let billing_address = payment_data.get_billing()?; let shipping_address = payment_data.get_shipping_address_with_phone_number()?; let address = payment_data.get_billing_address()?; + let line_items = payment_data + .request + .get_order_details()? + .iter() + .map(|order_detail| { + let price = convert_amount( + payment.amount_converter, + order_detail.amount, + payment_data.request.currency.ok_or_else(|| { + errors::ConnectorError::MissingRequiredField { + field_name: "currency", + } + })?, + )?; + + Ok(LineItem { + price, + quantity: i32::from(order_detail.quantity), + title: order_detail.product_name.clone(), + product_type: order_detail.product_type.clone(), + requires_shipping: order_detail.requires_shipping, + product_id: order_detail.product_id.clone(), + category: order_detail.category.clone(), + brand: order_detail.brand.clone(), + }) + }) + .collect::, Self::Error>>()?; Ok(Self { order: CheckoutRequest { @@ -156,23 +213,9 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutReq created_at: common_utils::date_time::now(), updated_at: common_utils::date_time::now(), gateway: payment_data.request.gateway.clone(), - total_price: payment_data.request.amount, + total_price: payment.amount.clone(), cart_token: payment_data.attempt_id.clone(), - line_items: payment_data - .request - .get_order_details()? - .iter() - .map(|order_detail| LineItem { - price: order_detail.amount.get_amount_as_i64(), // This should be changed to MinorUnit when we implement amount conversion for this connector. Additionally, the function get_amount_as_i64() should be avoided in the future. - quantity: i32::from(order_detail.quantity), - title: order_detail.product_name.clone(), - product_type: order_detail.product_type.clone(), - requires_shipping: order_detail.requires_shipping, - product_id: order_detail.product_id.clone(), - category: order_detail.category.clone(), - brand: order_detail.brand.clone(), - }) - .collect::>(), + line_items, source: Source::DesktopWeb, billing_address: OrderAddress::try_from(billing_address).ok(), shipping_address: OrderAddress::try_from(shipping_address).ok(), @@ -411,7 +454,7 @@ pub struct SuccessfulTransactionData { pub struct TransactionDecisionData { external_status: TransactionStatus, reason: Option, - amount: i64, + amount: StringMajorUnit, currency: storage_enums::Currency, #[serde(with = "common_utils::custom_serde::iso8601")] decided_at: PrimitiveDateTime, @@ -429,16 +472,21 @@ pub enum TransactionStatus { Approved, } -impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionSuccessRequest { +impl TryFrom<&RiskifiedRouterData<&frm_types::FrmTransactionRouterData>> + for TransactionSuccessRequest +{ type Error = Error; - fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + fn try_from( + item_data: &RiskifiedRouterData<&frm_types::FrmTransactionRouterData>, + ) -> Result { + let item = item_data.router_data.clone(); Ok(Self { order: SuccessfulTransactionData { id: item.attempt_id.clone(), decision: TransactionDecisionData { external_status: TransactionStatus::Approved, reason: None, - amount: item.request.amount, + amount: item_data.amount.clone(), currency: item.request.get_currency()?, decided_at: common_utils::date_time::now(), payment_details: [TransactionPaymentDetails { diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs index 2d1a42092f4..213aef9cf03 100644 --- a/crates/router/src/types/api/fraud_check.rs +++ b/crates/router/src/types/api/fraud_check.rs @@ -51,7 +51,7 @@ impl FraudCheckConnectorData { Ok(ConnectorEnum::Old(Box::new(&connector::Signifyd))) } enums::FrmConnectors::Riskified => { - Ok(ConnectorEnum::Old(Box::new(&connector::Riskified))) + Ok(ConnectorEnum::Old(Box::new(connector::Riskified::new()))) } } } From 9baa1ef65442c11dc57dd4b82a32c93f9542d45d Mon Sep 17 00:00:00 2001 From: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:50:33 +0530 Subject: [PATCH 16/51] ci(cypress): Add list and revoke for zero auth mandate payments (#6569) --- .../00013-ListAndRevokeMandate.cy.js | 53 ++++++++++++++++++- cypress-tests/cypress/support/commands.js | 8 +-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js index 9091135d539..f341db19f6c 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js @@ -4,7 +4,7 @@ import getConnectorDetails, * as utils from "../PaymentUtils/Utils"; let globalState; -describe("Card - SingleUse Mandates flow test", () => { +describe("Card - List and revoke Mandates flow test", () => { before("seed global state", () => { cy.task("getGlobalState").then((state) => { globalState = new State(state); @@ -71,4 +71,55 @@ describe("Card - SingleUse Mandates flow test", () => { }); } ); + context("Card - Zero auth CIT and MIT payment flow test", () => { + let should_continue = true; // variable that will be used to skip tests if a previous test fails + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Confirm No 3DS CIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "ZeroAuthMandate" + ]; + let req_data = data["Request"]; + let res_data = data["Response"]; + cy.citForMandatesCallTest( + fixtures.citConfirmBody, + req_data, + res_data, + 0, + true, + "automatic", + "setup_mandate", + globalState + ); + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("list-mandate-call-test", () => { + cy.listMandateCallTest(globalState); + }); + + it("Confirm No 3DS MIT", () => { + cy.mitForMandatesCallTest( + fixtures.mitConfirmBody, + 7000, + true, + "automatic", + globalState + ); + }); + + it("list-mandate-call-test", () => { + cy.listMandateCallTest(globalState); + }); + + it("revoke-mandate-call-test", () => { + cy.revokeMandateCallTest(globalState); + }); + }); }); diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 5ed6617ae42..3df6c901d97 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -1969,9 +1969,7 @@ Cypress.Commands.add( } else if (response.body.authentication_type === "no_three_ds") { if (response.body.connector === "fiuu") { expect(response.body.status).to.equal("failed"); - } else { - expect(response.body.status).to.equal("succeeded"); - } + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` @@ -2051,9 +2049,7 @@ Cypress.Commands.add( } else if (response.body.authentication_type === "no_three_ds") { if (response.body.connector === "fiuu") { expect(response.body.status).to.equal("failed"); - } else { - expect(response.body.status).to.equal("succeeded"); - } + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` From ea81432e3eb72d9a2e139e26741a42cdd8d31202 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:59:08 +0530 Subject: [PATCH 17/51] ci(cypress): add testcases for nti based mit flow (#6567) --- .../PaymentTest/00020-MandatesUsingNTID.cy.js | 221 ++++++++++++++++++ .../{00020-UPI.cy.js => 00021-UPI.cy.js} | 0 ...ariations.cy.js => 00022-Variations.cy.js} | 0 ...thods.cy.js => 00023-PaymentMethods.cy.js} | 0 ...ic.cy.js => 00024-ConnectorAgnostic.cy.js} | 0 .../cypress/fixtures/create-ntid-mit.json | 49 ++++ cypress-tests/cypress/fixtures/imports.js | 2 + cypress-tests/cypress/support/commands.js | 75 ++++++ 8 files changed, 347 insertions(+) create mode 100644 cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js rename cypress-tests/cypress/e2e/PaymentTest/{00020-UPI.cy.js => 00021-UPI.cy.js} (100%) rename cypress-tests/cypress/e2e/PaymentTest/{00021-Variations.cy.js => 00022-Variations.cy.js} (100%) rename cypress-tests/cypress/e2e/PaymentTest/{00022-PaymentMethods.cy.js => 00023-PaymentMethods.cy.js} (100%) rename cypress-tests/cypress/e2e/PaymentTest/{00023-ConnectorAgnostic.cy.js => 00024-ConnectorAgnostic.cy.js} (100%) create mode 100644 cypress-tests/cypress/fixtures/create-ntid-mit.json diff --git a/cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js new file mode 100644 index 00000000000..edd46f7f834 --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js @@ -0,0 +1,221 @@ +import * as fixtures from "../../fixtures/imports"; +import State from "../../utils/State"; +import getConnectorDetails, * as utils from "../PaymentUtils/Utils"; + +let globalState; +let connector; + +describe("Card - Mandates using Network Transaction Id flow test", () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + connector = globalState.get("connectorId"); + }); + }); + + afterEach("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + context( + "Card - NoThreeDS Create and Confirm Automatic MIT payment flow test", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue || connector !== "cybersource") { + this.skip(); + } + }); + + it("Confirm No 3DS MIT", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 7000, + true, + "automatic", + globalState + ); + }); + } + ); + + context( + "Card - NoThreeDS Create and Confirm Manual MIT payment flow test", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue || connector !== "cybersource") { + this.skip(); + } + }); + + it("Confirm No 3DS MIT", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 7000, + true, + "manual", + globalState + ); + }); + } + ); + + context( + "Card - NoThreeDS Create and Confirm Automatic multiple MITs payment flow test", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue || connector !== "cybersource") { + this.skip(); + } + }); + + it("Confirm No 3DS MIT", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 7000, + true, + "automatic", + globalState + ); + }); + it("Confirm No 3DS MIT", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 7000, + true, + "automatic", + globalState + ); + }); + } + ); + + context( + "Card - NoThreeDS Create and Confirm Manual multiple MITs payment flow test", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue || connector !== "cybersource") { + this.skip(); + } + }); + + it("Confirm No 3DS MIT 1", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 6500, + true, + "manual", + globalState + ); + }); + + it("mit-capture-call-test", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["Capture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.captureCallTest( + fixtures.captureBody, + req_data, + res_data, + 6500, + globalState + ); + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("Confirm No 3DS MIT 2", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 6500, + true, + "manual", + globalState + ); + }); + + it("mit-capture-call-test", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["Capture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.captureCallTest( + fixtures.captureBody, + req_data, + res_data, + 6500, + globalState + ); + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + } + ); + + context( + "Card - ThreeDS Create and Confirm Automatic multiple MITs payment flow test", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue || connector !== "cybersource") { + this.skip(); + } + }); + + it("Confirm No 3DS MIT", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 7000, + true, + "automatic", + globalState + ); + }); + it("Confirm No 3DS MIT", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 7000, + true, + "automatic", + globalState + ); + }); + } + ); + + context( + "Card - ThreeDS Create and Confirm Manual multiple MITs payment flow", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue || connector !== "cybersource") { + this.skip(); + } + }); + + it("Confirm No 3DS MIT", () => { + cy.mitUsingNTID( + fixtures.ntidConfirmBody, + 7000, + true, + "automatic", + globalState + ); + }); + } + ); +}); diff --git a/cypress-tests/cypress/e2e/PaymentTest/00020-UPI.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00021-UPI.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/PaymentTest/00020-UPI.cy.js rename to cypress-tests/cypress/e2e/PaymentTest/00021-UPI.cy.js diff --git a/cypress-tests/cypress/e2e/PaymentTest/00021-Variations.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00022-Variations.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/PaymentTest/00021-Variations.cy.js rename to cypress-tests/cypress/e2e/PaymentTest/00022-Variations.cy.js diff --git a/cypress-tests/cypress/e2e/PaymentTest/00022-PaymentMethods.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00023-PaymentMethods.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/PaymentTest/00022-PaymentMethods.cy.js rename to cypress-tests/cypress/e2e/PaymentTest/00023-PaymentMethods.cy.js diff --git a/cypress-tests/cypress/e2e/PaymentTest/00023-ConnectorAgnostic.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00024-ConnectorAgnostic.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/PaymentTest/00023-ConnectorAgnostic.cy.js rename to cypress-tests/cypress/e2e/PaymentTest/00024-ConnectorAgnostic.cy.js diff --git a/cypress-tests/cypress/fixtures/create-ntid-mit.json b/cypress-tests/cypress/fixtures/create-ntid-mit.json new file mode 100644 index 00000000000..93b7e95e2a0 --- /dev/null +++ b/cypress-tests/cypress/fixtures/create-ntid-mit.json @@ -0,0 +1,49 @@ +{ + "amount": 999, + "currency": "USD", + "confirm": true, + "payment_method": "card", + "return_url": "https://hyperswitch.io", + "email": "example@email.com", + "recurring_details": { + "type": "network_transaction_id_and_card_details", + "data": { + "card_number": "4242424242424242", + "card_exp_month": "11", + "card_exp_year": "2024", + "card_holder_name": "joseph Doe", + "network_transaction_id": "MCC5ZRGMI0925" + } + }, + "off_session": true, + "billing": { + "address": { + "first_name": "John", + "last_name": "Doe", + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + }, + "phone": { + "number": "9123456789", + "country_code": "+91" + } + }, + "browser_info": { + "ip_address": "129.0.0.1", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "en-US", + "color_depth": 30, + "screen_height": 1117, + "screen_width": 1728, + "time_zone": -330, + "java_enabled": true, + "java_script_enabled": true + } + } + \ No newline at end of file diff --git a/cypress-tests/cypress/fixtures/imports.js b/cypress-tests/cypress/fixtures/imports.js index 900e4f4ea3a..49fea00491f 100644 --- a/cypress-tests/cypress/fixtures/imports.js +++ b/cypress-tests/cypress/fixtures/imports.js @@ -24,6 +24,7 @@ import updateBusinessProfile from "./update-business-profile.json"; import updateConnectorBody from "./update-connector-body.json"; import customerUpdateBody from "./update-customer-body.json"; import voidBody from "./void-payment-body.json"; +import ntidConfirmBody from "./create-ntid-mit.json"; export { apiKeyCreateBody, @@ -44,6 +45,7 @@ export { merchantCreateBody, merchantUpdateBody, mitConfirmBody, + ntidConfirmBody, pmIdConfirmBody, refundBody, routingConfigBody, diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 3df6c901d97..48a62fb4388 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -2087,6 +2087,81 @@ Cypress.Commands.add( } ); +Cypress.Commands.add( + "mitUsingNTID", + (requestBody, amount, confirm, capture_method, globalState) => { + + requestBody.amount = amount; + requestBody.confirm = confirm; + requestBody.capture_method = capture_method; + + if (globalState.get("connectorId") !== "cybersource") { + return; + } + + const apiKey = globalState.get("apiKey"); + const baseUrl = globalState.get("baseUrl"); + const url = `${baseUrl}/payments`; + + cy.request({ + method: "POST", + url: url, + headers: { + "Content-Type": "application/json", + "api-key": apiKey, + }, + failOnStatusCode: false, + body: requestBody, + }).then((response) => { + logRequestId(response.headers["x-request-id"]); + + if (response.status === 200) { + expect(response.headers["content-type"]).to.include("application/json"); + + globalState.set("paymentID", response.body.payment_id); + + if (response.body.capture_method === "automatic") { + if (response.body.authentication_type === "three_ds") { + expect(response.body) + .to.have.property("next_action") + .to.have.property("redirect_to_url"); + const nextActionUrl = response.body.next_action.redirect_to_url; + cy.log(nextActionUrl); + } else if (response.body.authentication_type === "no_three_ds") { + expect(response.body.status).to.equal("succeeded"); + } else { + throw new Error( + `Invalid authentication type ${response.body.authentication_type}` + ); + } + } else if (response.body.capture_method === "manual") { + if (response.body.authentication_type === "three_ds") { + expect(response.body) + .to.have.property("next_action") + .to.have.property("redirect_to_url"); + const nextActionUrl = response.body.next_action.redirect_to_url; + cy.log(nextActionUrl); + } else if (response.body.authentication_type === "no_three_ds") { + expect(response.body.status).to.equal("requires_capture"); + } else { + throw new Error( + `Invalid authentication type ${response.body.authentication_type}` + ); + } + } else { + throw new Error( + `Invalid capture method ${response.body.capture_method}` + ); + } + } else { + throw new Error( + `Error Response: ${response.status}\n${response.body.error.message}\n${response.body.error.code}` + ); + } + }); + } +); + Cypress.Commands.add("listMandateCallTest", (globalState) => { const customerId = globalState.get("customerId"); cy.request({ From 29a0885a8fc7b718f8b87866e2638e8bfad3c8f3 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:06:43 +0530 Subject: [PATCH 18/51] feat(router): [Cybersource] add PLN to the currency config (#6628) --- config/config.example.toml | 8 ++++---- config/deployments/integration_test.toml | 8 ++++---- config/deployments/production.toml | 8 ++++---- config/deployments/sandbox.toml | 8 ++++---- config/development.toml | 8 ++++---- config/docker_compose.toml | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 191f2ba7f8b..0436cea6b48 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -543,10 +543,10 @@ apple_pay = { currency = "USD" } google_pay = { currency = "USD" } [pm_filters.cybersource] -credit = { currency = "USD,GBP,EUR" } -debit = { currency = "USD,GBP,EUR" } -apple_pay = { currency = "USD,GBP,EUR" } -google_pay = { currency = "USD,GBP,EUR" } +credit = { currency = "USD,GBP,EUR,PLN" } +debit = { currency = "USD,GBP,EUR,PLN" } +apple_pay = { currency = "USD,GBP,EUR,PLN" } +google_pay = { currency = "USD,GBP,EUR,PLN" } samsung_pay = { currency = "USD,GBP,EUR" } paze = { currency = "USD" } diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 00a544dc565..ce6f38d84a4 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -309,10 +309,10 @@ klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,C sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } [pm_filters.cybersource] -credit = { currency = "USD,GBP,EUR" } -debit = { currency = "USD,GBP,EUR" } -apple_pay = { currency = "USD,GBP,EUR" } -google_pay = { currency = "USD,GBP,EUR" } +credit = { currency = "USD,GBP,EUR,PLN" } +debit = { currency = "USD,GBP,EUR,PLN" } +apple_pay = { currency = "USD,GBP,EUR,PLN" } +google_pay = { currency = "USD,GBP,EUR,PLN" } samsung_pay = { currency = "USD,GBP,EUR" } paze = { currency = "USD" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 0fe9095d280..81985e83bcc 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -283,10 +283,10 @@ google_pay = { currency = "USD" } [pm_filters.cybersource] -credit = { currency = "USD,GBP,EUR" } -debit = { currency = "USD,GBP,EUR" } -apple_pay = { currency = "USD,GBP,EUR" } -google_pay = { currency = "USD,GBP,EUR" } +credit = { currency = "USD,GBP,EUR,PLN" } +debit = { currency = "USD,GBP,EUR,PLN" } +apple_pay = { currency = "USD,GBP,EUR,PLN" } +google_pay = { currency = "USD,GBP,EUR,PLN" } samsung_pay = { currency = "USD,GBP,EUR" } paze = { currency = "USD" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 82c347ae389..4f98dc1ef09 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -286,10 +286,10 @@ apple_pay = { currency = "USD" } google_pay = { currency = "USD" } [pm_filters.cybersource] -credit = { currency = "USD,GBP,EUR" } -debit = { currency = "USD,GBP,EUR" } -apple_pay = { currency = "USD,GBP,EUR" } -google_pay = { currency = "USD,GBP,EUR" } +credit = { currency = "USD,GBP,EUR,PLN" } +debit = { currency = "USD,GBP,EUR,PLN" } +apple_pay = { currency = "USD,GBP,EUR,PLN" } +google_pay = { currency = "USD,GBP,EUR,PLN" } samsung_pay = { currency = "USD,GBP,EUR" } paze = { currency = "USD" } diff --git a/config/development.toml b/config/development.toml index ee6ea5dab0b..4cf2e1d4a80 100644 --- a/config/development.toml +++ b/config/development.toml @@ -461,10 +461,10 @@ google_pay = { currency = "USD" } [pm_filters.cybersource] -credit = { currency = "USD,GBP,EUR" } -debit = { currency = "USD,GBP,EUR" } -apple_pay = { currency = "USD,GBP,EUR" } -google_pay = { currency = "USD,GBP,EUR" } +credit = { currency = "USD,GBP,EUR,PLN" } +debit = { currency = "USD,GBP,EUR,PLN" } +apple_pay = { currency = "USD,GBP,EUR,PLN" } +google_pay = { currency = "USD,GBP,EUR,PLN" } samsung_pay = { currency = "USD,GBP,EUR" } paze = { currency = "USD" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index ed0ede98d94..bf7779863ca 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -462,10 +462,10 @@ apple_pay = { currency = "USD" } google_pay = { currency = "USD" } [pm_filters.cybersource] -credit = { currency = "USD,GBP,EUR" } -debit = { currency = "USD,GBP,EUR" } -apple_pay = { currency = "USD,GBP,EUR" } -google_pay = { currency = "USD,GBP,EUR" } +credit = { currency = "USD,GBP,EUR,PLN" } +debit = { currency = "USD,GBP,EUR,PLN" } +apple_pay = { currency = "USD,GBP,EUR,PLN" } +google_pay = { currency = "USD,GBP,EUR,PLN" } samsung_pay = { currency = "USD,GBP,EUR" } paze = { currency = "USD" } From 02479a12b18dc68e2787ae237580fcb46348374e Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:46:32 +0530 Subject: [PATCH 19/51] refactor(authn): Enable cookies in Integ (#6599) --- config/config.example.toml | 1 + config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/router/src/configs/settings.rs | 1 + crates/router/src/core/user.rs | 2 +- crates/router/src/routes/user.rs | 2 +- crates/router/src/services/authentication.rs | 71 +++++++++++++++++--- loadtest/config/development.toml | 1 + 11 files changed, 71 insertions(+), 12 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 0436cea6b48..4bdcdcf79df 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -403,6 +403,7 @@ two_factor_auth_expiry_in_secs = 300 # Number of seconds after which 2FA should totp_issuer_name = "Hyperswitch" # Name of the issuer for TOTP base_url = "" # Base url used for user specific redirects and emails force_two_factor_auth = false # Whether to force two factor authentication for all users +force_cookies = true # Whether to use only cookies for JWT extraction and authentication #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index ce6f38d84a4..a4e1b1e9b13 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -145,6 +145,7 @@ two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Integ" base_url = "https://integ.hyperswitch.io" force_two_factor_auth = false +force_cookies = true [frm] enabled = true diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 81985e83bcc..a859d08ac4a 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -152,6 +152,7 @@ two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Production" base_url = "https://live.hyperswitch.io" force_two_factor_auth = true +force_cookies = false [frm] enabled = false diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 4f98dc1ef09..070a32ef87b 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -152,6 +152,7 @@ two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Sandbox" base_url = "https://app.hyperswitch.io" force_two_factor_auth = false +force_cookies = false [frm] enabled = true diff --git a/config/development.toml b/config/development.toml index 4cf2e1d4a80..2388607a489 100644 --- a/config/development.toml +++ b/config/development.toml @@ -329,6 +329,7 @@ two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch Dev" base_url = "http://localhost:8080" force_two_factor_auth = false +force_cookies = true [bank_config.eps] stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index bf7779863ca..d72141d9c37 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -57,6 +57,7 @@ two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch" base_url = "http://localhost:8080" force_two_factor_auth = false +force_cookies = true [locker] host = "" diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 7b212ec6d1d..4e559a261b9 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -557,6 +557,7 @@ pub struct UserSettings { pub totp_issuer_name: String, pub base_url: String, pub force_two_factor_auth: bool, + pub force_cookies: bool, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index f01f6c5d749..c6501dac3bd 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -294,7 +294,7 @@ pub async fn connect_account( pub async fn signout( state: SessionState, - user_from_token: auth::UserFromToken, + user_from_token: auth::UserIdFromAuth, ) -> UserResponse<()> { tfa_utils::delete_totp_from_redis(&state, &user_from_token.user_id).await?; tfa_utils::delete_recovery_code_from_redis(&state, &user_from_token.user_id).await?; diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 068c2f30c79..8fc0dad452a 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -130,7 +130,7 @@ pub async fn signout(state: web::Data, http_req: HttpRequest) -> HttpR &http_req, (), |state, user, _, _| user_core::signout(state, user), - &auth::DashboardNoPermissionAuth, + &auth::AnyPurposeOrLoginTokenAuth, api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 2f5f55b8434..c05e4514aaa 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -871,6 +871,47 @@ where } } +#[cfg(feature = "olap")] +#[derive(Debug)] +pub struct AnyPurposeOrLoginTokenAuth; + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for AnyPurposeOrLoginTokenAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserIdFromAuth, AuthenticationType)> { + let payload = + parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + let purpose_exists = payload.purpose.is_some(); + let role_id_exists = payload.role_id.is_some(); + + if purpose_exists ^ role_id_exists { + Ok(( + UserIdFromAuth { + user_id: payload.user_id.clone(), + }, + AuthenticationType::SinglePurposeOrLoginJwt { + user_id: payload.user_id, + purpose: payload.purpose, + role_id: payload.role_id, + }, + )) + } else { + Err(errors::ApiErrorResponse::InvalidJwtToken.into()) + } + } +} + #[derive(Debug, Default)] pub struct AdminApiAuth; @@ -2504,17 +2545,27 @@ where T: serde::de::DeserializeOwned, A: SessionStateInfo + Sync, { - let token = match get_cookie_from_header(headers).and_then(cookies::parse_cookie) { - Ok(cookies) => cookies, - Err(error) => { - let token = get_jwt_from_authorization_header(headers); - if token.is_err() { - logger::error!(?error); - } - token?.to_owned() - } + let cookie_token_result = get_cookie_from_header(headers).and_then(cookies::parse_cookie); + let auth_header_token_result = get_jwt_from_authorization_header(headers); + let force_cookie = state.conf().user.force_cookies; + + logger::info!( + user_agent = ?headers.get(headers::USER_AGENT), + header_names = ?headers.keys().collect::>(), + is_token_equal = + auth_header_token_result.as_deref().ok() == cookie_token_result.as_deref().ok(), + cookie_error = ?cookie_token_result.as_ref().err(), + token_error = ?auth_header_token_result.as_ref().err(), + force_cookie, + ); + + let final_token = if force_cookie { + cookie_token_result? + } else { + auth_header_token_result?.to_owned() }; - decode_jwt(&token, state).await + + decode_jwt(&final_token, state).await } #[cfg(feature = "v1")] diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index dab85eb3cdd..a3ac1159ddb 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -36,6 +36,7 @@ password_validity_in_days = 90 two_factor_auth_expiry_in_secs = 300 totp_issuer_name = "Hyperswitch" force_two_factor_auth = false +force_cookies = true [locker] host = "" From 8fbb7663089d4790628109944e5fb5a57ccdaf00 Mon Sep 17 00:00:00 2001 From: Uzair Khan Date: Tue, 26 Nov 2024 18:54:42 +0530 Subject: [PATCH 20/51] feat(analytics): add `sessionized_metrics` for disputes analytics (#6573) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/analytics/src/clickhouse.rs | 4 + crates/analytics/src/disputes/accumulators.rs | 12 +- crates/analytics/src/disputes/core.rs | 46 ++++--- crates/analytics/src/disputes/metrics.rs | 16 +++ .../dispute_status_metric.rs | 120 ++++++++++++++++++ .../metrics/sessionized_metrics/mod.rs | 8 ++ .../total_amount_disputed.rs | 118 +++++++++++++++++ .../total_dispute_lost_amount.rs | 119 +++++++++++++++++ crates/analytics/src/sqlx.rs | 2 + crates/analytics/src/types.rs | 1 + crates/api_models/src/analytics.rs | 11 ++ crates/api_models/src/analytics/disputes.rs | 7 +- crates/api_models/src/events.rs | 6 + 13 files changed, 447 insertions(+), 23 deletions(-) create mode 100644 crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs create mode 100644 crates/analytics/src/disputes/metrics/sessionized_metrics/mod.rs create mode 100644 crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs create mode 100644 crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index 546b57f99af..f56e875f720 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -139,6 +139,9 @@ impl AnalyticsDataSource for ClickhouseClient { | AnalyticsCollection::Dispute => { TableEngine::CollapsingMergeTree { sign: "sign_flag" } } + AnalyticsCollection::DisputeSessionized => { + TableEngine::CollapsingMergeTree { sign: "sign_flag" } + } AnalyticsCollection::SdkEvents | AnalyticsCollection::SdkEventsAnalytics | AnalyticsCollection::ApiEvents @@ -439,6 +442,7 @@ impl ToSql for AnalyticsCollection { Self::ConnectorEvents => Ok("connector_events_audit".to_string()), Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), Self::Dispute => Ok("dispute".to_string()), + Self::DisputeSessionized => Ok("sessionizer_dispute".to_string()), Self::ActivePaymentsAnalytics => Ok("active_payments".to_string()), } } diff --git a/crates/analytics/src/disputes/accumulators.rs b/crates/analytics/src/disputes/accumulators.rs index 1997d75d323..41bd3beebdb 100644 --- a/crates/analytics/src/disputes/accumulators.rs +++ b/crates/analytics/src/disputes/accumulators.rs @@ -5,8 +5,8 @@ use super::metrics::DisputeMetricRow; #[derive(Debug, Default)] pub struct DisputeMetricsAccumulator { pub disputes_status_rate: RateAccumulator, - pub total_amount_disputed: SumAccumulator, - pub total_dispute_lost_amount: SumAccumulator, + pub disputed_amount: DisputedAmountAccumulator, + pub dispute_lost_amount: DisputedAmountAccumulator, } #[derive(Debug, Default)] pub struct RateAccumulator { @@ -17,7 +17,7 @@ pub struct RateAccumulator { } #[derive(Debug, Default)] #[repr(transparent)] -pub struct SumAccumulator { +pub struct DisputedAmountAccumulator { pub total: Option, } @@ -29,7 +29,7 @@ pub trait DisputeMetricAccumulator { fn collect(self) -> Self::MetricOutput; } -impl DisputeMetricAccumulator for SumAccumulator { +impl DisputeMetricAccumulator for DisputedAmountAccumulator { type MetricOutput = Option; #[inline] fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow) { @@ -92,8 +92,8 @@ impl DisputeMetricsAccumulator { disputes_challenged: challenge_rate, disputes_won: won_rate, disputes_lost: lost_rate, - total_amount_disputed: self.total_amount_disputed.collect(), - total_dispute_lost_amount: self.total_dispute_lost_amount.collect(), + disputed_amount: self.disputed_amount.collect(), + dispute_lost_amount: self.dispute_lost_amount.collect(), total_dispute, } } diff --git a/crates/analytics/src/disputes/core.rs b/crates/analytics/src/disputes/core.rs index b8b44a757de..85d1a62a1d9 100644 --- a/crates/analytics/src/disputes/core.rs +++ b/crates/analytics/src/disputes/core.rs @@ -5,8 +5,8 @@ use api_models::analytics::{ DisputeDimensions, DisputeMetrics, DisputeMetricsBucketIdentifier, DisputeMetricsBucketResponse, }, - AnalyticsMetadata, DisputeFilterValue, DisputeFiltersResponse, GetDisputeFilterRequest, - GetDisputeMetricRequest, MetricsResponse, + DisputeFilterValue, DisputeFiltersResponse, DisputesAnalyticsMetadata, DisputesMetricsResponse, + GetDisputeFilterRequest, GetDisputeMetricRequest, }; use error_stack::ResultExt; use router_env::{ @@ -30,7 +30,7 @@ pub async fn get_metrics( pool: &AnalyticsProvider, auth: &AuthInfo, req: GetDisputeMetricRequest, -) -> AnalyticsResult> { +) -> AnalyticsResult> { let mut metrics_accumulator: HashMap< DisputeMetricsBucketIdentifier, DisputeMetricsAccumulator, @@ -87,14 +87,17 @@ pub async fn get_metrics( logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); let metrics_builder = metrics_accumulator.entry(id).or_default(); match metric { - DisputeMetrics::DisputeStatusMetric => metrics_builder + DisputeMetrics::DisputeStatusMetric + | DisputeMetrics::SessionizedDisputeStatusMetric => metrics_builder .disputes_status_rate .add_metrics_bucket(&value), - DisputeMetrics::TotalAmountDisputed => metrics_builder - .total_amount_disputed - .add_metrics_bucket(&value), - DisputeMetrics::TotalDisputeLostAmount => metrics_builder - .total_dispute_lost_amount + DisputeMetrics::TotalAmountDisputed + | DisputeMetrics::SessionizedTotalAmountDisputed => { + metrics_builder.disputed_amount.add_metrics_bucket(&value) + } + DisputeMetrics::TotalDisputeLostAmount + | DisputeMetrics::SessionizedTotalDisputeLostAmount => metrics_builder + .dispute_lost_amount .add_metrics_bucket(&value), } } @@ -105,18 +108,31 @@ pub async fn get_metrics( metrics_accumulator ); } + let mut total_disputed_amount = 0; + let mut total_dispute_lost_amount = 0; let query_data: Vec = metrics_accumulator .into_iter() - .map(|(id, val)| DisputeMetricsBucketResponse { - values: val.collect(), - dimensions: id, + .map(|(id, val)| { + let collected_values = val.collect(); + if let Some(amount) = collected_values.disputed_amount { + total_disputed_amount += amount; + } + if let Some(amount) = collected_values.dispute_lost_amount { + total_dispute_lost_amount += amount; + } + + DisputeMetricsBucketResponse { + values: collected_values, + dimensions: id, + } }) .collect(); - Ok(MetricsResponse { + Ok(DisputesMetricsResponse { query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, + meta_data: [DisputesAnalyticsMetadata { + total_disputed_amount: Some(total_disputed_amount), + total_dispute_lost_amount: Some(total_dispute_lost_amount), }], }) } diff --git a/crates/analytics/src/disputes/metrics.rs b/crates/analytics/src/disputes/metrics.rs index dd1aa3c1bbd..ad7ed81aaee 100644 --- a/crates/analytics/src/disputes/metrics.rs +++ b/crates/analytics/src/disputes/metrics.rs @@ -1,4 +1,5 @@ mod dispute_status_metric; +mod sessionized_metrics; mod total_amount_disputed; mod total_dispute_lost_amount; @@ -92,6 +93,21 @@ where .load_metrics(dimensions, auth, filters, granularity, time_range, pool) .await } + Self::SessionizedTotalAmountDisputed => { + sessionized_metrics::TotalAmountDisputed::default() + .load_metrics(dimensions, auth, filters, granularity, time_range, pool) + .await + } + Self::SessionizedDisputeStatusMetric => { + sessionized_metrics::DisputeStatusMetric::default() + .load_metrics(dimensions, auth, filters, granularity, time_range, pool) + .await + } + Self::SessionizedTotalDisputeLostAmount => { + sessionized_metrics::TotalDisputeLostAmount::default() + .load_metrics(dimensions, auth, filters, granularity, time_range, pool) + .await + } } } } diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs new file mode 100644 index 00000000000..c5c0b91a173 --- /dev/null +++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs @@ -0,0 +1,120 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + disputes::{DisputeDimensions, DisputeFilters, DisputeMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::DisputeMetricRow; +use crate::{ + enums::AuthInfo, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(crate) struct DisputeStatusMetric {} + +#[async_trait::async_trait] +impl super::DisputeMetric for DisputeStatusMetric +where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + auth: &AuthInfo, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::DisputeSessionized); + + for dim in dimensions { + query_builder.add_select_column(dim).switch()?; + } + + query_builder.add_select_column("dispute_status").switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + auth.set_filter_clause(&mut query_builder).switch()?; + + time_range.set_filter_clause(&mut query_builder).switch()?; + + for dim in dimensions { + query_builder.add_group_by_clause(dim).switch()?; + } + + query_builder + .add_group_by_clause("dispute_status") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + DisputeMetricsBucketIdentifier::new( + i.dispute_stage.as_ref().map(|i| i.0), + i.connector.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/mod.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/mod.rs new file mode 100644 index 00000000000..4d41194634d --- /dev/null +++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/mod.rs @@ -0,0 +1,8 @@ +mod dispute_status_metric; +mod total_amount_disputed; +mod total_dispute_lost_amount; +pub(super) use dispute_status_metric::DisputeStatusMetric; +pub(super) use total_amount_disputed::TotalAmountDisputed; +pub(super) use total_dispute_lost_amount::TotalDisputeLostAmount; + +pub use super::{DisputeMetric, DisputeMetricAnalytics, DisputeMetricRow}; diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs new file mode 100644 index 00000000000..0767bdaf85d --- /dev/null +++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs @@ -0,0 +1,118 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + disputes::{DisputeDimensions, DisputeFilters, DisputeMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::DisputeMetricRow; +use crate::{ + enums::AuthInfo, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(crate) struct TotalAmountDisputed {} + +#[async_trait::async_trait] +impl super::DisputeMetric for TotalAmountDisputed +where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + auth: &AuthInfo, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::DisputeSessionized); + + for dim in dimensions { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "dispute_amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + auth.set_filter_clause(&mut query_builder).switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + query_builder + .add_filter_clause("dispute_status", "dispute_won") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + DisputeMetricsBucketIdentifier::new( + i.dispute_stage.as_ref().map(|i| i.0), + i.connector.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs new file mode 100644 index 00000000000..f4f4d860862 --- /dev/null +++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs @@ -0,0 +1,119 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + disputes::{DisputeDimensions, DisputeFilters, DisputeMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::DisputeMetricRow; +use crate::{ + enums::AuthInfo, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(crate) struct TotalDisputeLostAmount {} + +#[async_trait::async_trait] +impl super::DisputeMetric for TotalDisputeLostAmount +where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[DisputeDimensions], + auth: &AuthInfo, + filters: &DisputeFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::DisputeMetricAnalytics, + { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::DisputeSessionized); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "dispute_amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + auth.set_filter_clause(&mut query_builder).switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause("dispute_status", "dispute_lost") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + DisputeMetricsBucketIdentifier::new( + i.dispute_stage.as_ref().map(|i| i.0), + i.connector.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 16523d5d0a7..1d91ce17c6a 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -932,6 +932,8 @@ impl ToSql for AnalyticsCollection { Self::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, Self::Dispute => Ok("dispute".to_string()), + Self::DisputeSessionized => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("DisputeSessionized table is not implemented for Sqlx"))?, } } } diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index 6bdd11fcd73..86056338106 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -38,6 +38,7 @@ pub enum AnalyticsCollection { ConnectorEvents, OutgoingWebhookEvent, Dispute, + DisputeSessionized, ApiEventsAnalytics, ActivePaymentsAnalytics, } diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 70c0e0e78dc..ee904652154 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -346,6 +346,11 @@ pub struct SdkEventFilterValue { pub values: Vec, } +#[derive(Debug, serde::Serialize)] +pub struct DisputesAnalyticsMetadata { + pub total_disputed_amount: Option, + pub total_dispute_lost_amount: Option, +} #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct MetricsResponse { @@ -373,6 +378,12 @@ pub struct RefundsMetricsResponse { pub query_data: Vec, pub meta_data: [RefundsAnalyticsMetadata; 1], } +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DisputesMetricsResponse { + pub query_data: Vec, + pub meta_data: [DisputesAnalyticsMetadata; 1], +} #[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetApiEventFiltersRequest { diff --git a/crates/api_models/src/analytics/disputes.rs b/crates/api_models/src/analytics/disputes.rs index edb85c129e6..2509d83e132 100644 --- a/crates/api_models/src/analytics/disputes.rs +++ b/crates/api_models/src/analytics/disputes.rs @@ -24,6 +24,9 @@ pub enum DisputeMetrics { DisputeStatusMetric, TotalAmountDisputed, TotalDisputeLostAmount, + SessionizedDisputeStatusMetric, + SessionizedTotalAmountDisputed, + SessionizedTotalDisputeLostAmount, } #[derive( @@ -122,8 +125,8 @@ pub struct DisputeMetricsBucketValue { pub disputes_challenged: Option, pub disputes_won: Option, pub disputes_lost: Option, - pub total_amount_disputed: Option, - pub total_dispute_lost_amount: Option, + pub disputed_amount: Option, + pub dispute_lost_amount: Option, pub total_dispute: Option, } #[derive(Debug, serde::Serialize)] diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index dad624ef87c..72a0d592513 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -173,6 +173,12 @@ impl ApiEventMetric for RefundsMetricsResponse { Some(ApiEventsType::Miscellaneous) } } + +impl ApiEventMetric for DisputesMetricsResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl ApiEventMetric for PaymentMethodIntentConfirmInternal { fn get_api_event_type(&self) -> Option { From e922f96cee7e34493f0022b0c56455357eddc4f8 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:54:55 +0530 Subject: [PATCH 21/51] feat: Added grpc based health check (#6441) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + config/deployments/env_specific.toml | 1 + crates/api_models/Cargo.toml | 1 + crates/api_models/src/health_check.rs | 13 ++ crates/external_services/build.rs | 18 ++- crates/external_services/src/grpc_client.rs | 35 +++- .../src/grpc_client/dynamic_routing.rs | 17 +- .../src/grpc_client/health_check_client.rs | 149 ++++++++++++++++++ crates/router/Cargo.toml | 2 +- crates/router/src/core/health_check.rs | 23 +++ crates/router/src/routes/health.rs | 15 ++ crates/storage_impl/src/errors.rs | 6 + proto/health_check.proto | 21 +++ 13 files changed, 285 insertions(+), 17 deletions(-) create mode 100644 crates/external_services/src/grpc_client/health_check_client.rs create mode 100644 proto/health_check.proto diff --git a/config/config.example.toml b/config/config.example.toml index 4bdcdcf79df..ba14ed881cf 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -790,3 +790,4 @@ connector_list = "cybersource" # Supported connectors for network tokenization [grpc_client.dynamic_routing_client] # Dynamic Routing Client Configuration host = "localhost" # Client Host port = 7000 # Client Port +service = "dynamo" # Service name diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 0eab330652a..fa0c0484a77 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -326,3 +326,4 @@ check_token_status_url= "" # base url to check token status from token servic [grpc_client.dynamic_routing_client] # Dynamic Routing Client Configuration host = "localhost" # Client Host port = 7000 # Client Port +service = "dynamo" # Service name diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index e8668aab502..a5d702e26fb 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -20,6 +20,7 @@ v1 = ["common_utils/v1"] v2 = ["common_utils/v2", "customer_v2"] customer_v2 = ["common_utils/customer_v2"] payment_methods_v2 = ["common_utils/payment_methods_v2"] +dynamic_routing = [] [dependencies] actix-web = { version = "4.5.1", optional = true } diff --git a/crates/api_models/src/health_check.rs b/crates/api_models/src/health_check.rs index 1e86e2964c7..4a1c009e43e 100644 --- a/crates/api_models/src/health_check.rs +++ b/crates/api_models/src/health_check.rs @@ -1,3 +1,4 @@ +use std::collections::hash_map::HashMap; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RouterHealthCheckResponse { pub database: bool, @@ -9,10 +10,22 @@ pub struct RouterHealthCheckResponse { #[cfg(feature = "olap")] pub opensearch: bool, pub outgoing_request: bool, + #[cfg(feature = "dynamic_routing")] + pub grpc_health_check: HealthCheckMap, } impl common_utils::events::ApiEventMetric for RouterHealthCheckResponse {} +/// gRPC based services eligible for Health check +#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum HealthCheckServices { + /// Dynamic routing service + DynamicRoutingService, +} + +pub type HealthCheckMap = HashMap; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SchedulerHealthCheckResponse { pub database: bool, diff --git a/crates/external_services/build.rs b/crates/external_services/build.rs index 605ef699715..0a4938e94b4 100644 --- a/crates/external_services/build.rs +++ b/crates/external_services/build.rs @@ -3,11 +3,21 @@ fn main() -> Result<(), Box> { #[cfg(feature = "dynamic_routing")] { // Get the directory of the current crate - let proto_file = router_env::workspace_path() - .join("proto") - .join("success_rate.proto"); + + let proto_path = router_env::workspace_path().join("proto"); + let success_rate_proto_file = proto_path.join("success_rate.proto"); + + let health_check_proto_file = proto_path.join("health_check.proto"); + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); + // Compile the .proto file - tonic_build::compile_protos(proto_file).expect("Failed to compile success rate proto file"); + tonic_build::configure() + .out_dir(out_dir) + .compile( + &[success_rate_proto_file, health_check_proto_file], + &[proto_path], + ) + .expect("Failed to compile proto files"); } Ok(()) } diff --git a/crates/external_services/src/grpc_client.rs b/crates/external_services/src/grpc_client.rs index e7b229a8070..8981a1094d6 100644 --- a/crates/external_services/src/grpc_client.rs +++ b/crates/external_services/src/grpc_client.rs @@ -1,11 +1,28 @@ /// Dyanimc Routing Client interface implementation #[cfg(feature = "dynamic_routing")] pub mod dynamic_routing; +/// gRPC based Heath Check Client interface implementation +#[cfg(feature = "dynamic_routing")] +pub mod health_check_client; use std::{fmt::Debug, sync::Arc}; #[cfg(feature = "dynamic_routing")] use dynamic_routing::{DynamicRoutingClientConfig, RoutingStrategy}; +#[cfg(feature = "dynamic_routing")] +use health_check_client::HealthCheckClient; +#[cfg(feature = "dynamic_routing")] +use http_body_util::combinators::UnsyncBoxBody; +#[cfg(feature = "dynamic_routing")] +use hyper::body::Bytes; +#[cfg(feature = "dynamic_routing")] +use hyper_util::client::legacy::connect::HttpConnector; use serde; +#[cfg(feature = "dynamic_routing")] +use tonic::Status; + +#[cfg(feature = "dynamic_routing")] +/// Hyper based Client type for maintaining connection pool for all gRPC services +pub type Client = hyper_util::client::legacy::Client>; /// Struct contains all the gRPC Clients #[derive(Debug, Clone)] @@ -13,6 +30,9 @@ pub struct GrpcClients { /// The routing client #[cfg(feature = "dynamic_routing")] pub dynamic_routing: RoutingStrategy, + /// Health Check client for all gRPC services + #[cfg(feature = "dynamic_routing")] + pub health_client: HealthCheckClient, } /// Type that contains the configs required to construct a gRPC client with its respective services. #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)] @@ -29,17 +49,30 @@ impl GrpcClientSettings { /// This function will be called at service startup. #[allow(clippy::expect_used)] pub async fn get_grpc_client_interface(&self) -> Arc { + #[cfg(feature = "dynamic_routing")] + let client = + hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) + .http2_only(true) + .build_http(); + #[cfg(feature = "dynamic_routing")] let dynamic_routing_connection = self .dynamic_routing_client .clone() - .get_dynamic_routing_connection() + .get_dynamic_routing_connection(client.clone()) .await .expect("Failed to establish a connection with the Dynamic Routing Server"); + #[cfg(feature = "dynamic_routing")] + let health_client = HealthCheckClient::build_connections(self, client) + .await + .expect("Failed to build gRPC connections"); + Arc::new(GrpcClients { #[cfg(feature = "dynamic_routing")] dynamic_routing: dynamic_routing_connection, + #[cfg(feature = "dynamic_routing")] + health_client, }) } } diff --git a/crates/external_services/src/grpc_client/dynamic_routing.rs b/crates/external_services/src/grpc_client/dynamic_routing.rs index 3264f065b51..7ec42de0d7c 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing.rs @@ -6,9 +6,6 @@ use api_models::routing::{ }; use common_utils::{errors::CustomResult, ext_traits::OptionExt, transformers::ForeignTryFrom}; use error_stack::ResultExt; -use http_body_util::combinators::UnsyncBoxBody; -use hyper::body::Bytes; -use hyper_util::client::legacy::connect::HttpConnector; use router_env::logger; use serde; use success_rate::{ @@ -18,7 +15,8 @@ use success_rate::{ InvalidateWindowsResponse, LabelWithStatus, UpdateSuccessRateWindowConfig, UpdateSuccessRateWindowRequest, UpdateSuccessRateWindowResponse, }; -use tonic::Status; + +use super::Client; #[allow( missing_docs, unused_qualifications, @@ -45,8 +43,6 @@ pub enum DynamicRoutingError { SuccessRateBasedRoutingFailure(String), } -type Client = hyper_util::client::legacy::Client>; - /// Type that consists of all the services provided by the client #[derive(Debug, Clone)] pub struct RoutingStrategy { @@ -64,6 +60,8 @@ pub enum DynamicRoutingClientConfig { host: String, /// The port of the client port: u16, + /// Service name + service: String, }, #[default] /// If the dynamic routing client config has been disabled @@ -74,13 +72,10 @@ impl DynamicRoutingClientConfig { /// establish connection with the server pub async fn get_dynamic_routing_connection( self, + client: Client, ) -> Result> { - let client = - hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .http2_only(true) - .build_http(); let success_rate_client = match self { - Self::Enabled { host, port } => { + Self::Enabled { host, port, .. } => { let uri = format!("http://{}:{}", host, port).parse::()?; logger::info!("Connection established with dynamic routing gRPC Server"); Some(SuccessRateCalculatorClient::with_origin(client, uri)) diff --git a/crates/external_services/src/grpc_client/health_check_client.rs b/crates/external_services/src/grpc_client/health_check_client.rs new file mode 100644 index 00000000000..94d78df7955 --- /dev/null +++ b/crates/external_services/src/grpc_client/health_check_client.rs @@ -0,0 +1,149 @@ +use std::{collections::HashMap, fmt::Debug}; + +use api_models::health_check::{HealthCheckMap, HealthCheckServices}; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; +use error_stack::ResultExt; +pub use health_check::{ + health_check_response::ServingStatus, health_client::HealthClient, HealthCheckRequest, + HealthCheckResponse, +}; +use router_env::logger; + +#[allow( + missing_docs, + unused_qualifications, + clippy::unwrap_used, + clippy::as_conversions, + clippy::use_self +)] +pub mod health_check { + tonic::include_proto!("grpc.health.v1"); +} + +use super::{Client, DynamicRoutingClientConfig, GrpcClientSettings}; + +/// Result type for Dynamic Routing +pub type HealthCheckResult = CustomResult; +/// Dynamic Routing Errors +#[derive(Debug, Clone, thiserror::Error)] +pub enum HealthCheckError { + /// The required input is missing + #[error("Missing fields: {0} for building the Health check connection")] + MissingFields(String), + /// Error from gRPC Server + #[error("Error from gRPC Server : {0}")] + ConnectionError(String), + /// status is invalid + #[error("Invalid Status from server")] + InvalidStatus, +} + +/// Health Check Client type +#[derive(Debug, Clone)] +pub struct HealthCheckClient { + /// Health clients for all gRPC based services + pub clients: HashMap>, +} + +impl HealthCheckClient { + /// Build connections to all gRPC services + pub async fn build_connections( + config: &GrpcClientSettings, + client: Client, + ) -> Result> { + let dynamic_routing_config = &config.dynamic_routing_client; + let connection = match dynamic_routing_config { + DynamicRoutingClientConfig::Enabled { + host, + port, + service, + } => Some((host.clone(), *port, service.clone())), + _ => None, + }; + + let mut client_map = HashMap::new(); + + if let Some(conn) = connection { + let uri = format!("http://{}:{}", conn.0, conn.1).parse::()?; + let health_client = HealthClient::with_origin(client, uri); + + client_map.insert(HealthCheckServices::DynamicRoutingService, health_client); + } + + Ok(Self { + clients: client_map, + }) + } + /// Perform health check for all services involved + pub async fn perform_health_check( + &self, + config: &GrpcClientSettings, + ) -> HealthCheckResult { + let dynamic_routing_config = &config.dynamic_routing_client; + let connection = match dynamic_routing_config { + DynamicRoutingClientConfig::Enabled { + host, + port, + service, + } => Some((host.clone(), *port, service.clone())), + _ => None, + }; + + let health_client = self + .clients + .get(&HealthCheckServices::DynamicRoutingService); + + // SAFETY : This is a safe cast as there exists a valid + // integer value for this variant + #[allow(clippy::as_conversions)] + let expected_status = ServingStatus::Serving as i32; + + let mut service_map = HealthCheckMap::new(); + + let health_check_succeed = connection + .as_ref() + .async_map(|conn| self.get_response_from_grpc_service(conn.2.clone(), health_client)) + .await + .transpose() + .change_context(HealthCheckError::ConnectionError( + "error calling dynamic routing service".to_string(), + )) + .map_err(|err| logger::error!(error=?err)) + .ok() + .flatten() + .is_some_and(|resp| resp.status == expected_status); + + connection.and_then(|_conn| { + service_map.insert( + HealthCheckServices::DynamicRoutingService, + health_check_succeed, + ) + }); + + Ok(service_map) + } + + async fn get_response_from_grpc_service( + &self, + service: String, + client: Option<&HealthClient>, + ) -> HealthCheckResult { + let request = tonic::Request::new(HealthCheckRequest { service }); + + let mut client = client + .ok_or(HealthCheckError::MissingFields( + "[health_client]".to_string(), + ))? + .clone(); + + let response = client + .check(request) + .await + .change_context(HealthCheckError::ConnectionError( + "Failed to call dynamic routing service".to_string(), + ))? + .into_inner(); + + Ok(response) + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index b794443a047..f6e1f0efc69 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -37,7 +37,7 @@ v2 = ["customer_v2", "payment_methods_v2", "common_default", "api_models/v2", "d v1 = ["common_default", "api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "storage_impl/v1", "hyperswitch_interfaces/v1", "kgraph_utils/v1", "common_utils/v1"] customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2", "storage_impl/customer_v2"] payment_methods_v2 = ["api_models/payment_methods_v2", "diesel_models/payment_methods_v2", "hyperswitch_domain_models/payment_methods_v2", "storage_impl/payment_methods_v2", "common_utils/payment_methods_v2"] -dynamic_routing = ["external_services/dynamic_routing", "storage_impl/dynamic_routing"] +dynamic_routing = ["external_services/dynamic_routing", "storage_impl/dynamic_routing", "api_models/dynamic_routing"] # Partial Auth # The feature reduces the overhead of the router authenticating the merchant for every request, and trusts on `x-merchant-id` header to be present in the request. diff --git a/crates/router/src/core/health_check.rs b/crates/router/src/core/health_check.rs index 83faee677d4..31e8cc75f5b 100644 --- a/crates/router/src/core/health_check.rs +++ b/crates/router/src/core/health_check.rs @@ -1,5 +1,7 @@ #[cfg(feature = "olap")] use analytics::health_check::HealthCheck; +#[cfg(feature = "dynamic_routing")] +use api_models::health_check::HealthCheckMap; use api_models::health_check::HealthState; use error_stack::ResultExt; use router_env::logger; @@ -28,6 +30,11 @@ pub trait HealthCheckInterface { async fn health_check_opensearch( &self, ) -> CustomResult; + + #[cfg(feature = "dynamic_routing")] + async fn health_check_grpc( + &self, + ) -> CustomResult; } #[async_trait::async_trait] @@ -158,4 +165,20 @@ impl HealthCheckInterface for app::SessionState { logger::debug!("Outgoing request successful"); Ok(HealthState::Running) } + + #[cfg(feature = "dynamic_routing")] + async fn health_check_grpc( + &self, + ) -> CustomResult { + let health_client = &self.grpc_client.health_client; + let grpc_config = &self.conf.grpc_client; + + let health_check_map = health_client + .perform_health_check(grpc_config) + .await + .change_context(errors::HealthCheckGRPCServiceError::FailedToCallService)?; + + logger::debug!("Health check successful"); + Ok(health_check_map) + } } diff --git a/crates/router/src/routes/health.rs b/crates/router/src/routes/health.rs index c1cf28d00ad..0f2e6333647 100644 --- a/crates/router/src/routes/health.rs +++ b/crates/router/src/routes/health.rs @@ -95,6 +95,19 @@ async fn deep_health_check_func( logger::debug!("Analytics health check end"); + logger::debug!("gRPC health check begin"); + + #[cfg(feature = "dynamic_routing")] + let grpc_health_check = state.health_check_grpc().await.map_err(|error| { + let message = error.to_string(); + error.change_context(errors::ApiErrorResponse::HealthCheckError { + component: "gRPC services", + message, + }) + })?; + + logger::debug!("gRPC health check end"); + logger::debug!("Opensearch health check begin"); #[cfg(feature = "olap")] @@ -129,6 +142,8 @@ async fn deep_health_check_func( #[cfg(feature = "olap")] opensearch: opensearch_status.into(), outgoing_request: outgoing_check.into(), + #[cfg(feature = "dynamic_routing")] + grpc_health_check, }; Ok(api::ApplicationResponse::Json(response)) diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 1cee96f49eb..10d7f4dc8e8 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -292,3 +292,9 @@ pub enum HealthCheckLockerError { #[error("Failed to establish Locker connection")] FailedToCallLocker, } + +#[derive(Debug, Clone, thiserror::Error)] +pub enum HealthCheckGRPCServiceError { + #[error("Failed to establish connection with gRPC service")] + FailedToCallService, +} diff --git a/proto/health_check.proto b/proto/health_check.proto new file mode 100644 index 00000000000..b246f59f690 --- /dev/null +++ b/proto/health_check.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package grpc.health.v1; + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + SERVICE_UNKNOWN = 3; // Used only by the Watch method. + } + ServingStatus status = 1; +} + +service Health { + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); +} \ No newline at end of file From 75fe9c0c285f640967af33b1d969af9ce48c5b17 Mon Sep 17 00:00:00 2001 From: Kashif Date: Tue, 26 Nov 2024 18:57:12 +0530 Subject: [PATCH 22/51] feat(payments): propagate additional payment method data for google pay during MIT (#6644) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 33 ++++++++++++ .../src/payment_method_data.rs | 12 ++++- .../router/src/core/payment_methods/cards.rs | 7 +++ .../payments/operations/payment_create.rs | 54 ++++++++++--------- .../router/src/core/payments/tokenization.rs | 29 +++++++--- 5 files changed, 102 insertions(+), 33 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 0343ee02119..0bb5e65213f 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -820,7 +820,9 @@ pub struct PaymentMethodResponse { pub enum PaymentMethodsData { Card(CardDetailsPaymentMethod), BankDetails(PaymentMethodDataBankCreds), + WalletDetails(PaymentMethodDataWalletInfo), } + #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CardDetailsPaymentMethod { pub last4_digits: Option, @@ -847,6 +849,37 @@ pub struct PaymentMethodDataBankCreds { pub connector_details: Vec, } +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct PaymentMethodDataWalletInfo { + /// Last 4 digits of the card number + pub last4: String, + /// The information of the payment method + pub card_network: String, + /// The type of payment method + #[serde(rename = "type")] + pub card_type: String, +} + +impl From for PaymentMethodDataWalletInfo { + fn from(item: payments::additional_info::WalletAdditionalDataForCard) -> Self { + Self { + last4: item.last4, + card_network: item.card_network, + card_type: item.card_type, + } + } +} + +impl From for payments::additional_info::WalletAdditionalDataForCard { + fn from(item: PaymentMethodDataWalletInfo) -> Self { + Self { + last4: item.last4, + card_network: item.card_network, + card_type: item.card_type, + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BankAccountTokenData { pub payment_method_type: api_enums::PaymentMethodType, diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 9e4d6cfbec5..ea994946ca8 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -1,5 +1,5 @@ use api_models::{ - mandates, + mandates, payment_methods, payments::{additional_info as payment_additional_types, ExtendedCardInfo}, }; use common_enums::enums as api_enums; @@ -1708,3 +1708,13 @@ impl From for ExtendedCardInfo { } } } + +impl From for payment_methods::PaymentMethodDataWalletInfo { + fn from(item: GooglePayWalletData) -> Self { + Self { + last4: item.info.card_details, + card_network: item.info.card_network, + card_type: item.pm_type, + } + } +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 4073b7a4d9b..955cc37b0d9 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -5453,6 +5453,7 @@ pub async fn get_masked_bank_details( PaymentMethodsData::BankDetails(bank_details) => Ok(Some(MaskedBankDetails { mask: bank_details.mask, })), + PaymentMethodsData::WalletDetails(_) => Ok(None), }, None => Err(report!(errors::ApiErrorResponse::InternalServerError)) .attach_printable("Unable to fetch payment method data"), @@ -5492,6 +5493,12 @@ pub async fn get_bank_account_connector_details( message: "Card is not a valid entity".to_string(), } .into()), + PaymentMethodsData::WalletDetails(_) => { + Err(errors::ApiErrorResponse::UnprocessableEntity { + message: "Wallet is not a valid entity".to_string(), + } + .into()) + } PaymentMethodsData::BankDetails(bank_details) => { let connector_details = bank_details .connector_details diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index dcc2ba6a289..697c06603da 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1132,33 +1132,39 @@ impl PaymentCreate { if additional_pm_data.is_none() { // If recurring payment is made using payment_method_id, then fetch payment_method_data from retrieved payment_method object - additional_pm_data = payment_method_info - .as_ref() - .and_then(|pm_info| { - pm_info - .payment_method_data - .clone() - .map(|x| x.into_inner().expose()) - .and_then(|v| { - serde_json::from_value::(v) - .map_err(|err| { - logger::error!( - "Unable to deserialize payment methods data: {:?}", - err - ) + additional_pm_data = payment_method_info.as_ref().and_then(|pm_info| { + pm_info + .payment_method_data + .clone() + .map(|x| x.into_inner().expose()) + .and_then(|v| { + serde_json::from_value::(v) + .map_err(|err| { + logger::error!( + "Unable to deserialize payment methods data: {:?}", + err + ) + }) + .ok() + }) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(card) => { + Some(api_models::payments::AdditionalPaymentData::Card(Box::new( + api::CardDetailFromLocker::from(card).into(), + ))) + } + PaymentMethodsData::WalletDetails(wallet) => match payment_method_type { + Some(enums::PaymentMethodType::GooglePay) => { + Some(api_models::payments::AdditionalPaymentData::Wallet { + apple_pay: None, + google_pay: Some(wallet.into()), }) - .ok() - }) - .and_then(|pmd| match pmd { - PaymentMethodsData::Card(crd) => { - Some(api::CardDetailFromLocker::from(crd)) } _ => None, - }) - }) - .map(|card| { - api_models::payments::AdditionalPaymentData::Card(Box::new(card.into())) - }); + }, + _ => None, + }) + }); }; let additional_pm_data_value = additional_pm_data diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index a6f3018ed0d..14f64cab153 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -5,7 +5,9 @@ use std::collections::HashMap; not(feature = "payment_methods_v2") ))] use api_models::payment_methods::PaymentMethodsData; -use api_models::payments::ConnectorMandateReferenceId; +use api_models::{ + payment_methods::PaymentMethodDataWalletInfo, payments::ConnectorMandateReferenceId, +}; use common_enums::{ConnectorMandateStatus, PaymentMethod}; use common_utils::{ crypto::Encryptable, @@ -248,15 +250,26 @@ where None => None, }; - let pm_card_details = resp.card.as_ref().map(|card| { - PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())) - }); + let optional_pm_details = match ( + resp.card.as_ref(), + save_payment_method_data.request.get_payment_method_data(), + ) { + (Some(card), _) => Some(PaymentMethodsData::Card( + CardDetailsPaymentMethod::from(card.clone()), + )), + ( + _, + domain::PaymentMethodData::Wallet(domain::WalletData::GooglePay(googlepay)), + ) => Some(PaymentMethodsData::WalletDetails( + PaymentMethodDataWalletInfo::from(googlepay), + )), + _ => None, + }; + let key_manager_state = state.into(); let pm_data_encrypted: Option>> = - pm_card_details - .async_map(|pm_card| { - create_encrypted_data(&key_manager_state, key_store, pm_card) - }) + optional_pm_details + .async_map(|pm| create_encrypted_data(&key_manager_state, key_store, pm)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) From 31204941ee24fe7b23344ba9b4a2615c46f33bb0 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:22:16 +0530 Subject: [PATCH 23/51] feat(connector): [Netcetera] add sca exemption (#6611) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../connector/netcetera/netcetera_types.rs | 26 +++++++++++++++++++ .../src/connector/netcetera/transformers.rs | 14 ++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/netcetera/netcetera_types.rs b/crates/router/src/connector/netcetera/netcetera_types.rs index 2718fb3b238..68a01b26fba 100644 --- a/crates/router/src/connector/netcetera/netcetera_types.rs +++ b/crates/router/src/connector/netcetera/netcetera_types.rs @@ -1391,6 +1391,32 @@ impl From for Browser { } } +impl From> for ThreeDSRequestor { + fn from(value: Option) -> Self { + // if sca exemption is provided, we need to set the challenge indicator to NoChallengeRequestedTransactionalRiskAnalysis + let three_ds_requestor_challenge_ind = + if let Some(common_enums::ScaExemptionType::TransactionRiskAnalysis) = value { + Some(SingleOrListElement::Single( + ThreeDSRequestorChallengeIndicator::NoChallengeRequestedTransactionalRiskAnalysis, + )) + } else { + None + }; + + Self { + three_ds_requestor_authentication_ind: ThreeDSRequestorAuthenticationIndicator::Payment, + three_ds_requestor_authentication_info: None, + three_ds_requestor_challenge_ind, + three_ds_requestor_prior_authentication_info: None, + three_ds_requestor_dec_req_ind: None, + three_ds_requestor_dec_max_time: None, + app_ip: None, + three_ds_requestor_spc_support: None, + spc_incomp_ind: None, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub enum ChallengeWindowSizeEnum { #[serde(rename = "01")] diff --git a/crates/router/src/connector/netcetera/transformers.rs b/crates/router/src/connector/netcetera/transformers.rs index adcfb17f14b..ab228d95a9e 100644 --- a/crates/router/src/connector/netcetera/transformers.rs +++ b/crates/router/src/connector/netcetera/transformers.rs @@ -456,18 +456,8 @@ impl TryFrom<&NetceteraRouterData<&types::authentication::ConnectorAuthenticatio let now = common_utils::date_time::now(); let request = item.router_data.request.clone(); let pre_authn_data = request.pre_authentication_data.clone(); - let three_ds_requestor = netcetera_types::ThreeDSRequestor { - three_ds_requestor_authentication_ind: - netcetera_types::ThreeDSRequestorAuthenticationIndicator::Payment, - three_ds_requestor_authentication_info: None, - three_ds_requestor_challenge_ind: None, - three_ds_requestor_prior_authentication_info: None, - three_ds_requestor_dec_req_ind: None, - three_ds_requestor_dec_max_time: None, - app_ip: None, - three_ds_requestor_spc_support: None, - spc_incomp_ind: None, - }; + let three_ds_requestor = + netcetera_types::ThreeDSRequestor::from(item.router_data.psd2_sca_exemption_type); let card = utils::get_card_details(request.payment_method_data, "netcetera")?; let cardholder_account = netcetera_types::CardholderAccount { acct_type: None, From 682947866e6afc197c71bbd255f22ae427704590 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:47:24 +0530 Subject: [PATCH 24/51] fix(core): add payment_id as query param in merchant return url (#6665) Co-authored-by: Chikke Srujan --- crates/router/src/core/payments/helpers.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d4f5b3b49f0..8435f09e8f3 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2948,11 +2948,13 @@ pub fn make_merchant_url_with_response( .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("Expected client secret to be `Some`")?; + let payment_id = redirection_response.payment_id.get_string_repr().to_owned(); let merchant_url_with_response = if business_profile.redirect_to_merchant_with_http_post { url::Url::parse_with_params( url, &[ ("status", status_check.to_string()), + ("payment_id", payment_id), ( "payment_intent_client_secret", payment_client_secret.peek().to_string(), @@ -2971,6 +2973,7 @@ pub fn make_merchant_url_with_response( url, &[ ("status", status_check.to_string()), + ("payment_id", payment_id), ( "payment_intent_client_secret", payment_client_secret.peek().to_string(), From d4b482c21cf57b022c7bbadc1a3a9c9d9e5d4f03 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 00:21:52 +0000 Subject: [PATCH 25/51] chore(version): 2024.11.27.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be9517667f..eb6f79e9624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.27.0 + +### Features + +- **analytics:** Add `sessionized_metrics` for disputes analytics ([#6573](https://github.com/juspay/hyperswitch/pull/6573)) ([`8fbb766`](https://github.com/juspay/hyperswitch/commit/8fbb7663089d4790628109944e5fb5a57ccdaf00)) +- **connector:** + - [INESPAY] add Connector Template Code ([#6614](https://github.com/juspay/hyperswitch/pull/6614)) ([`710186f`](https://github.com/juspay/hyperswitch/commit/710186f035c92a919e8f5a49565c6f8908f1803f)) + - [Netcetera] add sca exemption ([#6611](https://github.com/juspay/hyperswitch/pull/6611)) ([`3120494`](https://github.com/juspay/hyperswitch/commit/31204941ee24fe7b23344ba9b4a2615c46f33bb0)) +- **payments:** Propagate additional payment method data for google pay during MIT ([#6644](https://github.com/juspay/hyperswitch/pull/6644)) ([`75fe9c0`](https://github.com/juspay/hyperswitch/commit/75fe9c0c285f640967af33b1d969af9ce48c5b17)) +- **router:** [Cybersource] add PLN to the currency config ([#6628](https://github.com/juspay/hyperswitch/pull/6628)) ([`29a0885`](https://github.com/juspay/hyperswitch/commit/29a0885a8fc7b718f8b87866e2638e8bfad3c8f3)) +- **users:** Send welcome to community email in magic link signup ([#6639](https://github.com/juspay/hyperswitch/pull/6639)) ([`03423a1`](https://github.com/juspay/hyperswitch/commit/03423a1f76d324453052da985f998fd3f957ce90)) +- Added grpc based health check ([#6441](https://github.com/juspay/hyperswitch/pull/6441)) ([`e922f96`](https://github.com/juspay/hyperswitch/commit/e922f96cee7e34493f0022b0c56455357eddc4f8)) + +### Bug Fixes + +- **core:** Add payment_id as query param in merchant return url ([#6665](https://github.com/juspay/hyperswitch/pull/6665)) ([`6829478`](https://github.com/juspay/hyperswitch/commit/682947866e6afc197c71bbd255f22ae427704590)) + +### Refactors + +- **authn:** Enable cookies in Integ ([#6599](https://github.com/juspay/hyperswitch/pull/6599)) ([`02479a1`](https://github.com/juspay/hyperswitch/commit/02479a12b18dc68e2787ae237580fcb46348374e)) +- **connector:** Add amount conversion framework to Riskified ([#6359](https://github.com/juspay/hyperswitch/pull/6359)) ([`acb30ef`](https://github.com/juspay/hyperswitch/commit/acb30ef6d144eaf13b237b830d1ac534259932a3)) +- **payments_v2:** Use batch encryption for intent create and confirm intent ([#6589](https://github.com/juspay/hyperswitch/pull/6589)) ([`108b160`](https://github.com/juspay/hyperswitch/commit/108b1603fa44b2a56c278196edb5a1f76f5d3d03)) +- **tenant:** Use tenant id type ([#6643](https://github.com/juspay/hyperswitch/pull/6643)) ([`c9df7b0`](https://github.com/juspay/hyperswitch/commit/c9df7b0557889c88ea20392dfe56bf651e22c9a7)) + +**Full Changelog:** [`2024.11.26.0...2024.11.27.0`](https://github.com/juspay/hyperswitch/compare/2024.11.26.0...2024.11.27.0) + +- - - + ## 2024.11.26.0 ### Features From f3424b7576554215945f61b52f38e43bb1e5a8b7 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:37:10 +0530 Subject: [PATCH 26/51] fix(users): Check lineage across entities in invite (#6677) --- crates/router/src/core/user.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index c6501dac3bd..2087d01dbb4 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -642,6 +642,38 @@ async fn handle_existing_user_invitation( return Err(UserErrors::UserExists.into()); } + let (org_id, merchant_id, profile_id) = match role_info.get_entity_type() { + EntityType::Organization => (Some(&user_from_token.org_id), None, None), + EntityType::Merchant => ( + Some(&user_from_token.org_id), + Some(&user_from_token.merchant_id), + None, + ), + EntityType::Profile => ( + Some(&user_from_token.org_id), + Some(&user_from_token.merchant_id), + Some(&user_from_token.profile_id), + ), + }; + + if state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: invitee_user_from_db.get_user_id(), + org_id, + merchant_id, + profile_id, + entity_id: None, + version: None, + status: None, + limit: Some(1), + }) + .await + .is_ok_and(|data| data.is_empty().not()) + { + return Err(UserErrors::UserExists.into()); + } + let user_role = domain::NewUserRole { user_id: invitee_user_from_db.get_user_id().to_owned(), role_id: request.role_id.clone(), From 4b45d21269437479435302aa1ea7d3d741e2a009 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:35:53 +0530 Subject: [PATCH 27/51] refactor(core): add error handling wrapper to wehbook (#6636) --- crates/api_models/src/connector_enums.rs | 3 + .../src/connectors/cashtocode.rs | 3 +- .../src/connectors/worldline.rs | 3 +- .../src/connectors/zen.rs | 3 +- .../src/connectors/zsl.rs | 3 +- crates/hyperswitch_interfaces/src/webhooks.rs | 29 +++++- crates/router/src/connector/adyen.rs | 2 + crates/router/src/connector/adyenplatform.rs | 31 +++++-- crates/router/src/connector/braintree.rs | 2 + crates/router/src/core/webhooks/incoming.rs | 93 +++++++++++++++---- .../router/src/core/webhooks/incoming_v2.rs | 4 +- .../connector_integration_interface.rs | 8 +- 12 files changed, 151 insertions(+), 33 deletions(-) diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 783ecb12b48..3d027c026d7 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -282,6 +282,9 @@ impl Connector { pub fn is_pre_processing_required_before_authorize(&self) -> bool { matches!(self, Self::Airwallex) } + pub fn should_acknowledge_webhook_for_resource_not_found_errors(&self) -> bool { + matches!(self, Self::Adyenplatform) + } #[cfg(feature = "dummy_connector")] pub fn validate_dummy_connector_enabled( &self, diff --git a/crates/hyperswitch_connectors/src/connectors/cashtocode.rs b/crates/hyperswitch_connectors/src/connectors/cashtocode.rs index c72b63aebc0..1ee6f10c635 100644 --- a/crates/hyperswitch_connectors/src/connectors/cashtocode.rs +++ b/crates/hyperswitch_connectors/src/connectors/cashtocode.rs @@ -30,7 +30,7 @@ use hyperswitch_interfaces::{ errors, events::connector_api_logs::ConnectorEvent, types::{PaymentsAuthorizeType, Response}, - webhooks, + webhooks::{self, IncomingWebhookFlowError}, }; use masking::{Mask, PeekInterface, Secret}; use transformers as cashtocode; @@ -420,6 +420,7 @@ impl webhooks::IncomingWebhook for Cashtocode { fn get_webhook_api_response( &self, request: &webhooks::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { let status = "EXECUTED".to_string(); let obj: transformers::CashtocodePaymentsSyncResponse = request diff --git a/crates/hyperswitch_connectors/src/connectors/worldline.rs b/crates/hyperswitch_connectors/src/connectors/worldline.rs index c5a8466175c..e40d4817224 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldline.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldline.rs @@ -41,7 +41,7 @@ use hyperswitch_interfaces::{ PaymentsAuthorizeType, PaymentsCaptureType, PaymentsSyncType, PaymentsVoidType, RefundExecuteType, RefundSyncType, Response, }, - webhooks, + webhooks::{self, IncomingWebhookFlowError}, }; use masking::{ExposeInterface, Mask, PeekInterface}; use ring::hmac; @@ -814,6 +814,7 @@ impl webhooks::IncomingWebhook for Worldline { fn get_webhook_api_response( &self, request: &webhooks::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult< hyperswitch_domain_models::api::ApplicationResponse, errors::ConnectorError, diff --git a/crates/hyperswitch_connectors/src/connectors/zen.rs b/crates/hyperswitch_connectors/src/connectors/zen.rs index a90b10fbd86..5dc9e4d3176 100644 --- a/crates/hyperswitch_connectors/src/connectors/zen.rs +++ b/crates/hyperswitch_connectors/src/connectors/zen.rs @@ -41,7 +41,7 @@ use hyperswitch_interfaces::{ errors, events::connector_api_logs::ConnectorEvent, types::{PaymentsAuthorizeType, PaymentsSyncType, RefundExecuteType, RefundSyncType, Response}, - webhooks::{IncomingWebhook, IncomingWebhookRequestDetails}, + webhooks::{IncomingWebhook, IncomingWebhookFlowError, IncomingWebhookRequestDetails}, }; use masking::{Mask, PeekInterface, Secret}; use transformers::{self as zen, ZenPaymentStatus, ZenWebhookTxnType}; @@ -671,6 +671,7 @@ impl IncomingWebhook for Zen { fn get_webhook_api_response( &self, _request: &IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(ApplicationResponse::Json(serde_json::json!({ "status": "ok" diff --git a/crates/hyperswitch_connectors/src/connectors/zsl.rs b/crates/hyperswitch_connectors/src/connectors/zsl.rs index 0a833a1f84a..13007b72601 100644 --- a/crates/hyperswitch_connectors/src/connectors/zsl.rs +++ b/crates/hyperswitch_connectors/src/connectors/zsl.rs @@ -36,7 +36,7 @@ use hyperswitch_interfaces::{ errors, events::connector_api_logs::ConnectorEvent, types::{self, Response}, - webhooks::{IncomingWebhook, IncomingWebhookRequestDetails}, + webhooks::{IncomingWebhook, IncomingWebhookFlowError, IncomingWebhookRequestDetails}, }; use masking::{ExposeInterface, Secret}; use transformers::{self as zsl, get_status}; @@ -442,6 +442,7 @@ impl IncomingWebhook for Zsl { fn get_webhook_api_response( &self, _request: &IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(ApplicationResponse::TextPlain("CALLBACK-OK".to_string())) } diff --git a/crates/hyperswitch_interfaces/src/webhooks.rs b/crates/hyperswitch_interfaces/src/webhooks.rs index f5240aed9ca..9a1a3f99793 100644 --- a/crates/hyperswitch_interfaces/src/webhooks.rs +++ b/crates/hyperswitch_interfaces/src/webhooks.rs @@ -2,7 +2,9 @@ use common_utils::{crypto, errors::CustomResult, ext_traits::ValueExt}; use error_stack::ResultExt; -use hyperswitch_domain_models::api::ApplicationResponse; +use hyperswitch_domain_models::{ + api::ApplicationResponse, errors::api_error_response::ApiErrorResponse, +}; use masking::{ExposeInterface, Secret}; use crate::{api::ConnectorCommon, errors}; @@ -22,6 +24,30 @@ pub struct IncomingWebhookRequestDetails<'a> { pub query_params: String, } +/// IncomingWebhookFlowError enum defining the error type for incoming webhook +#[derive(Debug)] +pub enum IncomingWebhookFlowError { + /// Resource not found for the webhook + ResourceNotFound, + /// Internal error for the webhook + InternalError, +} + +impl From<&ApiErrorResponse> for IncomingWebhookFlowError { + fn from(api_error_response: &ApiErrorResponse) -> Self { + match api_error_response { + ApiErrorResponse::WebhookResourceNotFound + | ApiErrorResponse::DisputeNotFound { .. } + | ApiErrorResponse::PayoutNotFound + | ApiErrorResponse::MandateNotFound + | ApiErrorResponse::PaymentNotFound + | ApiErrorResponse::RefundNotFound + | ApiErrorResponse::AuthenticationNotFound { .. } => Self::ResourceNotFound, + _ => Self::InternalError, + } + } +} + /// Trait defining incoming webhook #[async_trait::async_trait] pub trait IncomingWebhook: ConnectorCommon + Sync { @@ -203,6 +229,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { fn get_webhook_api_response( &self, _request: &IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(ApplicationResponse::StatusOk) } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index c9ccd8a9c62..f3b7414a923 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -7,6 +7,7 @@ use common_utils::{ }; use diesel_models::{enums as storage_enums, enums}; use error_stack::{report, ResultExt}; +use hyperswitch_interfaces::webhooks::IncomingWebhookFlowError; use masking::{ExposeInterface, Secret}; use ring::hmac; use router_env::{instrument, tracing}; @@ -1880,6 +1881,7 @@ impl api::IncomingWebhook for Adyen { fn get_webhook_api_response( &self, _request: &api::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(services::api::ApplicationResponse::TextPlain( diff --git a/crates/router/src/connector/adyenplatform.rs b/crates/router/src/connector/adyenplatform.rs index 3da1a2d33be..2ee80699a5a 100644 --- a/crates/router/src/connector/adyenplatform.rs +++ b/crates/router/src/connector/adyenplatform.rs @@ -13,6 +13,8 @@ use error_stack::report; use error_stack::ResultExt; #[cfg(feature = "payouts")] use http::HeaderName; +use hyperswitch_interfaces::webhooks::IncomingWebhookFlowError; +use masking::Maskable; #[cfg(feature = "payouts")] use masking::Secret; #[cfg(feature = "payouts")] @@ -27,11 +29,7 @@ use crate::{ configs::settings, core::errors::{self, CustomResult}, headers, - services::{ - self, - request::{self, Mask}, - ConnectorValidation, - }, + services::{self, request::Mask, ConnectorValidation}, types::{ self, api::{self, ConnectorCommon}, @@ -67,7 +65,7 @@ impl ConnectorCommon for Adyenplatform { fn get_auth_header( &self, auth_type: &types::ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { let auth = adyenplatform::AdyenplatformAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![( @@ -209,7 +207,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), types::PayoutFulfillType::get_content_type(self) @@ -401,6 +399,25 @@ impl api::IncomingWebhook for Adyenplatform { } } + fn get_webhook_api_response( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + error_kind: Option, + ) -> CustomResult, errors::ConnectorError> + { + if error_kind.is_some() { + Ok(services::api::ApplicationResponse::JsonWithHeaders(( + serde_json::Value::Null, + vec![( + "x-http-code".to_string(), + Maskable::Masked(Secret::new("404".to_string())), + )], + ))) + } else { + Ok(services::api::ApplicationResponse::StatusOk) + } + } + fn get_webhook_event_type( &self, #[cfg(feature = "payouts")] request: &api::IncomingWebhookRequestDetails<'_>, diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index dba26121880..40155f7117f 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -10,6 +10,7 @@ use common_utils::{ }; use diesel_models::enums; use error_stack::{report, Report, ResultExt}; +use hyperswitch_interfaces::webhooks::IncomingWebhookFlowError; use masking::{ExposeInterface, PeekInterface, Secret}; use ring::hmac; use sha1::{Digest, Sha1}; @@ -980,6 +981,7 @@ impl api::IncomingWebhook for Braintree { fn get_webhook_api_response( &self, _request: &api::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(services::api::ApplicationResponse::TextPlain( diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 3532f1e3fd7..5eb489da788 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -12,7 +12,7 @@ use hyperswitch_domain_models::{ router_request_types::VerifyWebhookSourceRequestData, router_response_types::{VerifyWebhookSourceResponseData, VerifyWebhookStatus}, }; -use hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails; +use hyperswitch_interfaces::webhooks::{IncomingWebhookFlowError, IncomingWebhookRequestDetails}; use masking::{ExposeInterface, PeekInterface}; use router_env::{instrument, metrics::add_attributes, tracing, tracing_actix_web::RequestId}; @@ -209,7 +209,7 @@ async fn incoming_webhooks_core( ); let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Failed while early return in case of event type parsing")?; @@ -260,14 +260,25 @@ async fn incoming_webhooks_core( let merchant_connector_account = match merchant_connector_account { Some(merchant_connector_account) => merchant_connector_account, None => { - Box::pin(helper_utils::get_mca_from_object_reference_id( + match Box::pin(helper_utils::get_mca_from_object_reference_id( &state, object_ref_id.clone(), &merchant_account, &connector_name, &key_store, )) - .await? + .await + { + Ok(mca) => mca, + Err(error) => { + return handle_incoming_webhook_error( + error, + &connector, + connector_name.as_str(), + &request_details, + ); + } + } } }; @@ -358,7 +369,7 @@ async fn incoming_webhooks_core( id: profile_id.get_string_repr().to_owned(), })?; - match flow_type { + let result_response = match flow_type { api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow( state.clone(), req_state, @@ -372,7 +383,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for payments failed")?, + .attach_printable("Incoming webhook flow for payments failed"), api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow( state.clone(), @@ -385,7 +396,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for refunds failed")?, + .attach_printable("Incoming webhook flow for refunds failed"), api::WebhookFlow::Dispute => Box::pin(disputes_incoming_webhook_flow( state.clone(), @@ -399,7 +410,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for disputes failed")?, + .attach_printable("Incoming webhook flow for disputes failed"), api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow( state.clone(), @@ -411,9 +422,9 @@ async fn incoming_webhooks_core( source_verified, )) .await - .attach_printable("Incoming bank-transfer webhook flow failed")?, + .attach_printable("Incoming bank-transfer webhook flow failed"), - api::WebhookFlow::ReturnResponse => WebhookResponseTracker::NoEffect, + api::WebhookFlow::ReturnResponse => Ok(WebhookResponseTracker::NoEffect), api::WebhookFlow::Mandate => Box::pin(mandates_incoming_webhook_flow( state.clone(), @@ -425,7 +436,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for mandates failed")?, + .attach_printable("Incoming webhook flow for mandates failed"), api::WebhookFlow::ExternalAuthentication => { Box::pin(external_authentication_incoming_webhook_flow( @@ -442,7 +453,7 @@ async fn incoming_webhooks_core( merchant_connector_account, )) .await - .attach_printable("Incoming webhook flow for external authentication failed")? + .attach_printable("Incoming webhook flow for external authentication failed") } api::WebhookFlow::FraudCheck => Box::pin(frm_incoming_webhook_flow( state.clone(), @@ -455,7 +466,7 @@ async fn incoming_webhooks_core( business_profile, )) .await - .attach_printable("Incoming webhook flow for fraud check failed")?, + .attach_printable("Incoming webhook flow for fraud check failed"), #[cfg(feature = "payouts")] api::WebhookFlow::Payout => Box::pin(payouts_incoming_webhook_flow( @@ -468,10 +479,22 @@ async fn incoming_webhooks_core( source_verified, )) .await - .attach_printable("Incoming webhook flow for payouts failed")?, + .attach_printable("Incoming webhook flow for payouts failed"), _ => Err(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unsupported Flow Type received in incoming webhooks")?, + .attach_printable("Unsupported Flow Type received in incoming webhooks"), + }; + + match result_response { + Ok(response) => response, + Err(error) => { + return handle_incoming_webhook_error( + error, + &connector, + connector_name.as_str(), + &request_details, + ); + } } } else { metrics::WEBHOOK_INCOMING_FILTERED_COUNT.add( @@ -486,7 +509,7 @@ async fn incoming_webhooks_core( }; let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Could not get incoming webhook api response from connector")?; @@ -497,6 +520,44 @@ async fn incoming_webhooks_core( Ok((response, webhook_effect, serialized_request)) } +fn handle_incoming_webhook_error( + error: error_stack::Report, + connector: &ConnectorEnum, + connector_name: &str, + request_details: &IncomingWebhookRequestDetails<'_>, +) -> errors::RouterResult<( + services::ApplicationResponse, + WebhookResponseTracker, + serde_json::Value, +)> { + logger::error!(?error, "Incoming webhook flow failed"); + + // fetch the connector enum from the connector name + let connector_enum = api_models::connector_enums::Connector::from_str(connector_name) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + + // get the error response from the connector + if connector_enum.should_acknowledge_webhook_for_resource_not_found_errors() { + let response = connector + .get_webhook_api_response( + request_details, + Some(IncomingWebhookFlowError::from(error.current_context())), + ) + .switch() + .attach_printable("Failed to get incoming webhook api response from connector")?; + Ok(( + response, + WebhookResponseTracker::NoEffect, + serde_json::Value::Null, + )) + } else { + Err(error) + } +} + #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] async fn payments_incoming_webhook_flow( diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs index 0deb91efaa8..c1734794fcc 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -192,7 +192,7 @@ async fn incoming_webhooks_core( ); let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Failed while early return in case of event type parsing")?; @@ -367,7 +367,7 @@ async fn incoming_webhooks_core( }; let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Could not get incoming webhook api response from connector")?; diff --git a/crates/router/src/services/connector_integration_interface.rs b/crates/router/src/services/connector_integration_interface.rs index 53690227f59..ccbf65917f1 100644 --- a/crates/router/src/services/connector_integration_interface.rs +++ b/crates/router/src/services/connector_integration_interface.rs @@ -1,7 +1,8 @@ use common_utils::{crypto, errors::CustomResult, request::Request}; use hyperswitch_domain_models::{router_data::RouterData, router_data_v2::RouterDataV2}; use hyperswitch_interfaces::{ - authentication::ExternalAuthenticationPayload, connector_integration_v2::ConnectorIntegrationV2, + authentication::ExternalAuthenticationPayload, + connector_integration_v2::ConnectorIntegrationV2, webhooks::IncomingWebhookFlowError, }; use super::{BoxedConnectorIntegrationV2, ConnectorValidation}; @@ -279,11 +280,12 @@ impl api::IncomingWebhook for ConnectorEnum { fn get_webhook_api_response( &self, request: &IncomingWebhookRequestDetails<'_>, + error_kind: Option, ) -> CustomResult, errors::ConnectorError> { match self { - Self::Old(connector) => connector.get_webhook_api_response(request), - Self::New(connector) => connector.get_webhook_api_response(request), + Self::Old(connector) => connector.get_webhook_api_response(request, error_kind), + Self::New(connector) => connector.get_webhook_api_response(request, error_kind), } } From b1c4e30e929fb8d31d854765d5f1ddad5e77f065 Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Wed, 27 Nov 2024 23:48:13 +0530 Subject: [PATCH 28/51] ci(cypress): Move MIT requests to configs and add Paybox Mandates (#6650) --- .../e2e/PaymentTest/00009-RefundPayment.cy.js | 14 + .../00011-CreateSingleuseMandate.cy.js | 21 + .../00012-CreateMultiuseMandate.cy.js | 35 ++ .../00013-ListAndRevokeMandate.cy.js | 14 + .../PaymentTest/00015-ZeroAuthMandate.cy.js | 21 + .../PaymentTest/00019-MandatesUsingPMID.cy.js | 63 +++ .../PaymentTest/00020-MandatesUsingNTID.cy.js | 63 +++ .../e2e/PaymentTest/00022-Variations.cy.js | 37 +- .../cypress/e2e/PaymentUtils/Adyen.js | 18 + .../cypress/e2e/PaymentUtils/BankOfAmerica.js | 18 + .../cypress/e2e/PaymentUtils/Cybersource.js | 18 + .../cypress/e2e/PaymentUtils/Fiuu.js | 22 + .../cypress/e2e/PaymentUtils/Noon.js | 18 + .../cypress/e2e/PaymentUtils/Paybox.js | 531 +++++++++++++++++- .../cypress/e2e/PaymentUtils/Stripe.js | 18 + .../cypress/e2e/PaymentUtils/WellsFargo.js | 18 + cypress-tests/cypress/support/commands.js | 102 +++- 17 files changed, 988 insertions(+), 43 deletions(-) diff --git a/cypress-tests/cypress/e2e/PaymentTest/00009-RefundPayment.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00009-RefundPayment.cy.js index 9eb64c0acf2..4eef1ac8737 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00009-RefundPayment.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00009-RefundPayment.cy.js @@ -815,8 +815,15 @@ describe("Card - Refund flow - No 3DS", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -825,8 +832,15 @@ describe("Card - Refund flow - No 3DS", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", diff --git a/cypress-tests/cypress/e2e/PaymentTest/00011-CreateSingleuseMandate.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00011-CreateSingleuseMandate.cy.js index 5c140913826..bace2a78018 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00011-CreateSingleuseMandate.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00011-CreateSingleuseMandate.cy.js @@ -49,8 +49,15 @@ describe("Card - SingleUse Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -112,8 +119,15 @@ describe("Card - SingleUse Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 6500, true, "manual", @@ -197,8 +211,15 @@ describe("Card - SingleUse Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", diff --git a/cypress-tests/cypress/e2e/PaymentTest/00012-CreateMultiuseMandate.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00012-CreateMultiuseMandate.cy.js index 3596af70fe1..312c74c23c0 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00012-CreateMultiuseMandate.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00012-CreateMultiuseMandate.cy.js @@ -49,8 +49,15 @@ describe("Card - MultiUse Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -58,8 +65,15 @@ describe("Card - MultiUse Mandates flow test", () => { ); }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -121,8 +135,15 @@ describe("Card - MultiUse Mandates flow test", () => { }); it("Confirm No 3DS MIT 1", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 6500, true, "manual", @@ -149,8 +170,15 @@ describe("Card - MultiUse Mandates flow test", () => { }); it("Confirm No 3DS MIT 2", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 6500, true, "manual", @@ -230,8 +258,15 @@ describe("Card - MultiUse Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 6500, true, "automatic", diff --git a/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js index f341db19f6c..cad829ba310 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00013-ListAndRevokeMandate.cy.js @@ -49,8 +49,15 @@ describe("Card - List and revoke Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -105,8 +112,15 @@ describe("Card - List and revoke Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "MITAutoCapture" + ]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", diff --git a/cypress-tests/cypress/e2e/PaymentTest/00015-ZeroAuthMandate.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00015-ZeroAuthMandate.cy.js index b9083142727..15e7597286a 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00015-ZeroAuthMandate.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00015-ZeroAuthMandate.cy.js @@ -47,8 +47,15 @@ describe("Card - SingleUse Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -89,8 +96,15 @@ describe("Card - SingleUse Mandates flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -98,8 +112,15 @@ describe("Card - SingleUse Mandates flow test", () => { ); }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 7000, true, "automatic", diff --git a/cypress-tests/cypress/e2e/PaymentTest/00019-MandatesUsingPMID.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00019-MandatesUsingPMID.cy.js index b2951e37586..401f662c67e 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00019-MandatesUsingPMID.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00019-MandatesUsingPMID.cy.js @@ -67,8 +67,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -148,8 +155,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -193,8 +207,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -202,8 +223,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { ); }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -265,8 +293,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { }); it("Confirm No 3DS MIT 1", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 6500, true, "manual", @@ -293,8 +328,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { }); it("Confirm No 3DS MIT 2", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 6500, true, "manual", @@ -361,8 +403,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -370,8 +419,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { ); }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -438,8 +494,15 @@ describe("Card - Mandates using Payment Method Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingPMId( fixtures.pmIdConfirmBody, + req_data, + res_data, 7000, true, "automatic", diff --git a/cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js index edd46f7f834..dcccfd390bf 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00020-MandatesUsingNTID.cy.js @@ -29,8 +29,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -52,8 +59,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 7000, true, "manual", @@ -75,8 +89,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -84,8 +105,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { ); }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -107,8 +135,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { }); it("Confirm No 3DS MIT 1", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 6500, true, "manual", @@ -135,8 +170,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { }); it("Confirm No 3DS MIT 2", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITManualCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 6500, true, "manual", @@ -176,8 +218,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -185,8 +234,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { ); }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 7000, true, "automatic", @@ -208,8 +264,15 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitUsingNTID( fixtures.ntidConfirmBody, + req_data, + res_data, 7000, true, "automatic", diff --git a/cypress-tests/cypress/e2e/PaymentTest/00022-Variations.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00022-Variations.cy.js index b24b9f10f79..035b7da97ab 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00022-Variations.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00022-Variations.cy.js @@ -463,9 +463,15 @@ describe("Corner cases", () => { let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ "Void" ]; - let commonData = getConnectorDetails(globalState.get("commons"))["card_pm"]["Void"]; + let commonData = getConnectorDetails(globalState.get("commons"))[ + "card_pm" + ]["Void"]; let req_data = data["Request"]; - let res_data = utils.getConnectorFlowDetails(data, commonData, "ResponseCustom"); + let res_data = utils.getConnectorFlowDetails( + data, + commonData, + "ResponseCustom" + ); cy.voidCallTest(fixtures.voidBody, req_data, res_data, globalState); if (should_continue) @@ -595,9 +601,15 @@ describe("Corner cases", () => { let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ "Refund" ]; - let commonData = getConnectorDetails(globalState.get("commons"))["card_pm"]["Refund"]; + let commonData = getConnectorDetails(globalState.get("commons"))[ + "card_pm" + ]["Refund"]; let req_data = data["Request"]; - let res_data = utils.getConnectorFlowDetails(data, commonData, "ResponseCustom"); + let res_data = utils.getConnectorFlowDetails( + data, + commonData, + "ResponseCustom" + ); cy.refundCallTest( fixtures.refundBody, req_data, @@ -658,9 +670,15 @@ describe("Corner cases", () => { let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ "Refund" ]; - let commonData = getConnectorDetails(globalState.get("commons"))["card_pm"]["Refund"]; + let commonData = getConnectorDetails(globalState.get("commons"))[ + "card_pm" + ]["Refund"]; let req_data = data["Request"]; - let res_data = utils.getConnectorFlowDetails(data, commonData, "ResponseCustom"); + let res_data = utils.getConnectorFlowDetails( + data, + commonData, + "ResponseCustom" + ); cy.refundCallTest( fixtures.refundBody, req_data, @@ -734,8 +752,15 @@ describe("Corner cases", () => { }); it("Confirm No 3DS MIT", () => { + let data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "MITAutoCapture" + ]; + let req_data = data["Request"]; + let res_data = data["Response"]; cy.mitForMandatesCallTest( fixtures.mitConfirmBody, + req_data, + res_data, 65000, true, "manual", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js b/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js index 9ce384c7a74..81525041147 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js @@ -398,6 +398,24 @@ export const connectorDetails = { }, }, }, + MITAutoCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MITManualCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, ZeroAuthMandate: { Request: { payment_method: "card", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js b/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js index 4a443e0962a..af4dee5d537 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js @@ -395,6 +395,24 @@ export const connectorDetails = { }, }, }, + MITAutoCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MITManualCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, ZeroAuthMandate: { Request: { payment_method: "card", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js b/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js index ca24a50bcef..72dc8f6479b 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js @@ -458,6 +458,24 @@ export const connectorDetails = { }, }, }, + MITAutoCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MITManualCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, ZeroAuthMandate: { Request: { payment_method: "card", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js b/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js index 4a910c7897c..d82a5f128d6 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Fiuu.js @@ -360,6 +360,28 @@ export const connectorDetails = { }, }, }, + MITAutoCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "failed", + error_code: "The currency not allow for the RecordType", + error_message: "The currency not allow for the RecordType", + }, + }, + }, + MITManualCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "failed", + error_code: "The currency not allow for the RecordType", + error_message: "The currency not allow for the RecordType", + }, + }, + }, PaymentMethodIdMandateNo3DSAutoCapture: { Request: { payment_method: "card", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Noon.js b/cypress-tests/cypress/e2e/PaymentUtils/Noon.js index 70c056b6c92..38b0c0c1117 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Noon.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Noon.js @@ -468,6 +468,24 @@ export const connectorDetails = { }, }, }, + MITAutoCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MITManualCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, ZeroAuthMandate: { Response: { status: 501, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js index d1eb91692da..70bfa5c3240 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js @@ -6,6 +6,57 @@ const successfulNo3DSCardDetails = { card_cvc: "222", }; +const successfulThreeDSTestCardDetails = { + card_number: "4000000000001091", + card_exp_month: "01", + card_exp_year: "25", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; + +const singleUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + single_use: { + amount: 7000, + currency: "EUR", + }, + }, +}; + +const multiUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + multi_use: { + amount: 6500, + currency: "EUR", + }, + }, +}; + +const customerAcceptance = { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, +}; + export const connectorDetails = { card_pm: { PaymentIntent: { @@ -21,6 +72,22 @@ export const connectorDetails = { }, }, }, + PaymentIntentOffSession: { + Request: { + currency: "EUR", + amount: 6500, + authentication_type: "no_three_ds", + customer_acceptance: null, + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", + }, + }, + }, No3DSManualCapture: { Request: { currency: "EUR", @@ -52,7 +119,43 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "processing", + status: "succeeded", + }, + }, + }, + "3DSManualCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + setup_future_usage: "on_session", + }, + }, + }, + "3DSAutoCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + setup_future_usage: "on_session", }, }, }, @@ -67,10 +170,10 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "processing", + status: "succeeded", amount: 6500, - amount_capturable: 6500, - amount_received: null, + amount_capturable: 0, + amount_received: 6500, }, }, }, @@ -79,10 +182,24 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "processing", + status: "partially_captured", amount: 6500, - amount_capturable: 6500, - amount_received: null, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + VoidAfterConfirm: { + Request: {}, + Response: { + status: 501, + body: { + status: "cancelled", + error: { + type: "invalid_request", + message: "Cancel/Void flow is not implemented", + code: "IR_00", + }, }, }, }, @@ -131,7 +248,405 @@ export const connectorDetails = { }, }, }, - + MandateSingleUse3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: + "Capture Not allowed in case of Creating the Subscriber is not supported by Paybox", + }, + }, + }, + }, + MandateSingleUse3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + }, + }, + }, + MandateSingleUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: + "Capture Not allowed in case of Creating the Subscriber is not supported by Paybox", + }, + }, + }, + }, + MandateSingleUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MandateMultiUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: + "Capture Not allowed in case of Creating the Subscriber is not supported by Paybox", + }, + }, + }, + }, + MandateMultiUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MandateMultiUse3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: multiUseMandateData, + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: + "Capture Not allowed in case of Creating the Subscriber is not supported by Paybox", + }, + }, + }, + }, + MandateMultiUse3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MITAutoCapture: { + Request: { + currency: "EUR", + amount: 6500, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MITManualCapture: { + Request: { + currency: "EUR", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + PaymentMethodIdMandateNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: null, + customer_acceptance: customerAcceptance, + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: + "Capture Not allowed in case of Creating the Subscriber is not supported by Paybox", + }, + }, + }, + }, + PaymentMethodIdMandateNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: null, + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + PaymentMethodIdMandate3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + currency: "EUR", + mandate_data: null, + authentication_type: "three_ds", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: + "Capture Not allowed in case of Creating the Subscriber is not supported by Paybox", + }, + }, + }, + }, + PaymentMethodIdMandate3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDSTestCardDetails, + }, + mandate_data: null, + authentication_type: "three_ds", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + }, + }, + }, + ZeroAuthMandate: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + ZeroAuthPaymentIntent: { + Request: { + amount: 0, + setup_future_usage: "off_session", + currency: "EUR", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", + }, + }, + }, + ZeroAuthConfirmPayment: { + Request: { + payment_type: "setup_mandate", + payment_method: "card", + payment_method_type: "credit", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + }, + Response: { + status: 200, + body: { + status: "processing", + }, + }, + }, + SaveCardUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_type: "debit", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + setup_future_usage: "on_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSAutoCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_type: "debit", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + setup_future_usage: "off_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: "Payment method type not supported", + code: "IR_19", + reason: + "Capture Not allowed in case of Creating the Subscriber is not supported by Paybox", + }, + }, + }, + }, + SaveCardUseNo3DSManualCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_type: "debit", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + setup_future_usage: "off_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardConfirmAutoCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardConfirmManualCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "EUR", + setup_future_usage: "on_session", + customer_acceptance: customerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, InvalidCardNumber: { Request: { currency: "EUR", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js b/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js index 98af56b882c..984fe63b2fb 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js @@ -448,6 +448,24 @@ export const connectorDetails = { }, }, }, + MITAutoCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MITManualCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, ZeroAuthMandate: { Request: { payment_method: "card", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js b/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js index b25a6dc7e8b..608343f1083 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js @@ -362,6 +362,24 @@ export const connectorDetails = { }, }, }, + MITAutoCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MITManualCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, ZeroAuthMandate: { Request: { payment_method: "card", diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 48a62fb4388..17c2270efef 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -1926,7 +1926,18 @@ Cypress.Commands.add( Cypress.Commands.add( "mitForMandatesCallTest", - (requestBody, amount, confirm, capture_method, globalState) => { + ( + requestBody, + req_data, + res_data, + amount, + confirm, + capture_method, + globalState + ) => { + for (const key in req_data) { + requestBody[key] = req_data[key]; + } requestBody.amount = amount; requestBody.confirm = confirm; requestBody.capture_method = capture_method; @@ -1966,10 +1977,13 @@ Cypress.Commands.add( .to.have.property("redirect_to_url"); const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else if (response.body.authentication_type === "no_three_ds") { - if (response.body.connector === "fiuu") { - expect(response.body.status).to.equal("failed"); - } + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` @@ -1982,11 +1996,12 @@ Cypress.Commands.add( .to.have.property("redirect_to_url"); const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else if (response.body.authentication_type === "no_three_ds") { - if (response.body.connector === "fiuu") { - expect(response.body.status).to.equal("failed"); - } else { - expect(response.body.status).to.equal("requires_capture"); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); } } else { throw new Error( @@ -2009,9 +2024,7 @@ Cypress.Commands.add( ); } } else { - throw new Error( - `Error Response: ${response.status}\n${response.body.error.message}\n${response.body.error.code}` - ); + defaultErrorHandler(response, res_data); } }); } @@ -2019,7 +2032,18 @@ Cypress.Commands.add( Cypress.Commands.add( "mitUsingPMId", - (requestBody, amount, confirm, capture_method, globalState) => { + ( + requestBody, + req_data, + res_data, + amount, + confirm, + capture_method, + globalState + ) => { + for (const key in req_data) { + requestBody[key] = req_data[key]; + } requestBody.amount = amount; requestBody.confirm = confirm; requestBody.capture_method = capture_method; @@ -2046,10 +2070,13 @@ Cypress.Commands.add( .to.have.property("redirect_to_url"); const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else if (response.body.authentication_type === "no_three_ds") { - if (response.body.connector === "fiuu") { - expect(response.body.status).to.equal("failed"); - } + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` @@ -2062,11 +2089,12 @@ Cypress.Commands.add( .to.have.property("redirect_to_url"); const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else if (response.body.authentication_type === "no_three_ds") { - if (response.body.connector === "fiuu") { - expect(response.body.status).to.equal("failed"); - } else { - expect(response.body.status).to.equal("requires_capture"); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); } } else { throw new Error( @@ -2079,9 +2107,7 @@ Cypress.Commands.add( ); } } else { - throw new Error( - `Error Response: ${response.status}\n${response.body.error.message}\n${response.body.error.code}` - ); + defaultErrorHandler(response, res_data); } }); } @@ -2089,8 +2115,18 @@ Cypress.Commands.add( Cypress.Commands.add( "mitUsingNTID", - (requestBody, amount, confirm, capture_method, globalState) => { - + ( + requestBody, + req_data, + res_data, + amount, + confirm, + capture_method, + globalState + ) => { + for (const key in req_data) { + requestBody[key] = req_data[key]; + } requestBody.amount = amount; requestBody.confirm = confirm; requestBody.capture_method = capture_method; @@ -2127,8 +2163,13 @@ Cypress.Commands.add( .to.have.property("redirect_to_url"); const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else if (response.body.authentication_type === "no_three_ds") { - expect(response.body.status).to.equal("succeeded"); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` @@ -2141,8 +2182,13 @@ Cypress.Commands.add( .to.have.property("redirect_to_url"); const nextActionUrl = response.body.next_action.redirect_to_url; cy.log(nextActionUrl); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else if (response.body.authentication_type === "no_three_ds") { - expect(response.body.status).to.equal("requires_capture"); + for (const key in res_data.body) { + expect(res_data.body[key], [key]).to.equal(response.body[key]); + } } else { throw new Error( `Invalid authentication type ${response.body.authentication_type}` @@ -2154,9 +2200,7 @@ Cypress.Commands.add( ); } } else { - throw new Error( - `Error Response: ${response.status}\n${response.body.error.message}\n${response.body.error.code}` - ); + defaultErrorHandler(response, res_data); } }); } From 9be012826abe87ffa2d0cea5423aed3e50449de2 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:22:13 +0000 Subject: [PATCH 29/51] chore(version): 2024.11.28.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6f79e9624..dd80af8cf7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.28.0 + +### Bug Fixes + +- **users:** Check lineage across entities in invite ([#6677](https://github.com/juspay/hyperswitch/pull/6677)) ([`f3424b7`](https://github.com/juspay/hyperswitch/commit/f3424b7576554215945f61b52f38e43bb1e5a8b7)) + +### Refactors + +- **core:** Add error handling wrapper to wehbook ([#6636](https://github.com/juspay/hyperswitch/pull/6636)) ([`4b45d21`](https://github.com/juspay/hyperswitch/commit/4b45d21269437479435302aa1ea7d3d741e2a009)) + +**Full Changelog:** [`2024.11.27.0...2024.11.28.0`](https://github.com/juspay/hyperswitch/compare/2024.11.27.0...2024.11.28.0) + +- - - + ## 2024.11.27.0 ### Features From 2c865156a24ab834e690e2fa3a1c3584f4831b50 Mon Sep 17 00:00:00 2001 From: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:38:31 +0530 Subject: [PATCH 30/51] ci(cypress): Added Config Testcases (#6622) Co-authored-by: Pa1NarK <69745008+pixincreate@users.noreply.github.com> --- .../PaymentTest/00024-ConnectorAgnostic.cy.js | 38 +- .../e2e/PaymentTest/00025-ConfigTest.cy.js | 407 ++++++++++++++++++ .../cypress/fixtures/business-profile.json | 13 + .../fixtures/create-business-profile.json | 3 - cypress-tests/cypress/fixtures/imports.js | 6 +- .../fixtures/update-business-profile.json | 3 - cypress-tests/cypress/support/commands.js | 63 ++- 7 files changed, 512 insertions(+), 21 deletions(-) create mode 100644 cypress-tests/cypress/e2e/PaymentTest/00025-ConfigTest.cy.js create mode 100644 cypress-tests/cypress/fixtures/business-profile.json delete mode 100644 cypress-tests/cypress/fixtures/create-business-profile.json delete mode 100644 cypress-tests/cypress/fixtures/update-business-profile.json diff --git a/cypress-tests/cypress/e2e/PaymentTest/00024-ConnectorAgnostic.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00024-ConnectorAgnostic.cy.js index 64abe296b29..29b0097253d 100644 --- a/cypress-tests/cypress/e2e/PaymentTest/00024-ConnectorAgnostic.cy.js +++ b/cypress-tests/cypress/e2e/PaymentTest/00024-ConnectorAgnostic.cy.js @@ -26,7 +26,7 @@ describe("Connector Agnostic Tests", () => { it("Create Business Profile", () => { cy.createBusinessProfileTest( - fixtures.createBusinessProfile, + fixtures.businessProfile.bpCreate, globalState ); }); @@ -85,7 +85,7 @@ describe("Connector Agnostic Tests", () => { it("Create Business Profile", () => { cy.createBusinessProfileTest( - fixtures.createBusinessProfile, + fixtures.businessProfile.bpCreate, globalState ); }); @@ -101,8 +101,12 @@ describe("Connector Agnostic Tests", () => { it("Enable Connector Agnostic for Business Profile", () => { cy.UpdateBusinessProfileTest( - fixtures.updateBusinessProfile, - true, + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector globalState ); }); @@ -141,7 +145,10 @@ describe("Connector Agnostic Tests", () => { }); it("Create Business Profile", () => { - cy.createBusinessProfileTest(fixtures.createBusinessProfile, globalState); + cy.createBusinessProfileTest( + fixtures.businessProfile.bpCreate, + globalState + ); }); it("connector-create-call-test", () => { @@ -159,8 +166,12 @@ describe("Connector Agnostic Tests", () => { it("Enable Connector Agnostic for Business Profile", () => { cy.UpdateBusinessProfileTest( - fixtures.updateBusinessProfile, - true, + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector globalState ); }); @@ -205,7 +216,10 @@ describe("Connector Agnostic Tests", () => { }); it("Create Business Profile", () => { - cy.createBusinessProfileTest(fixtures.createBusinessProfile, globalState); + cy.createBusinessProfileTest( + fixtures.businessProfile.bpCreate, + globalState + ); }); it("connector-create-call-test", () => { @@ -219,8 +233,12 @@ describe("Connector Agnostic Tests", () => { it("Enable Connector Agnostic for Business Profile", () => { cy.UpdateBusinessProfileTest( - fixtures.updateBusinessProfile, - true, + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector globalState ); }); diff --git a/cypress-tests/cypress/e2e/PaymentTest/00025-ConfigTest.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00025-ConfigTest.cy.js new file mode 100644 index 00000000000..724233852c7 --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentTest/00025-ConfigTest.cy.js @@ -0,0 +1,407 @@ +import * as fixtures from "../../fixtures/imports"; +import State from "../../utils/State"; +import { payment_methods_enabled } from "../PaymentUtils/Commons"; +import getConnectorDetails, * as utils from "../PaymentUtils/Utils"; + +let globalState; + +describe("Config Tests", () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + context( + "Update collect_billing_details_from_wallet_connector to true and verifying in payment method list, this config should be true", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Create Business Profile", () => { + cy.createBusinessProfileTest( + fixtures.businessProfile.bpCreate, + globalState + ); + }); + + it("connector-create-call-test", () => { + cy.createConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + payment_methods_enabled, + globalState + ); + }); + + it("Create Customer", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + it("Update collect_billing_details_from_wallet_connector to true", () => { + cy.UpdateBusinessProfileTest( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + true, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("payment_methods-call-test", () => { + cy.paymentMethodsCallTest(globalState); + }); + } + ); + + context( + "Update collect_shipping_details_from_wallet_connector to true and verifying in payment method list, this config should be true", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Update collect_shipping_details_from_wallet_connector to true", () => { + cy.UpdateBusinessProfileTest( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("payment_methods-call-test", () => { + cy.paymentMethodsCallTest(globalState); + }); + } + ); + + context( + "Update always_collect_billing_details_from_wallet_connector to true and verifying in payment method list, this config should be true", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Update always_collect_billing_details_from_wallet_connector to true", () => { + cy.UpdateBusinessProfileTest( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + true, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("payment_methods-call-test", () => { + cy.paymentMethodsCallTest(globalState); + }); + } + ); + + context( + "Update always_collect_shipping_details_from_wallet_connector to true and verifying in payment method list, this config should be true", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Update always_collect_shipping_details_from_wallet_connector to true", () => { + cy.UpdateBusinessProfileTest( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + true, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("payment_methods-call-test", () => { + cy.paymentMethodsCallTest(globalState); + }); + } + ); + + context( + "Update always_collect_shipping_details_from_wallet_connector & collect_shipping_details_from_wallet_connector to true and verifying in payment method list, this config should be true", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Update both always & collect_shipping_details_from_wallet_connector to true", () => { + cy.UpdateBusinessProfileTest( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + true, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + true, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("payment_methods-call-test", () => { + cy.paymentMethodsCallTest(globalState); + }); + } + ); + context( + "Update always_collect_billing_details_from_wallet_connector & to collect_billing_details_from_wallet_connector to true and verifying in payment method list, this config should be true", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Update both always & collect_billing_details_from_wallet_connector to true", () => { + cy.UpdateBusinessProfileTest( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + true, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + true, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("payment_methods-call-test", () => { + cy.paymentMethodsCallTest(globalState); + }); + } + ); + + context( + "Update all config(Collect address config) to false and verifying in payment method list, both config should be false", + () => { + let should_continue = true; + + beforeEach(function () { + if (!should_continue) { + this.skip(); + } + }); + + it("Create Business Profile", () => { + cy.createBusinessProfileTest( + fixtures.businessProfile.bpCreate, + globalState + ); + }); + + it("connector-create-call-test", () => { + cy.createConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + payment_methods_enabled, + globalState + ); + }); + + it("Create Customer", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + it("Update all config to false", () => { + cy.UpdateBusinessProfileTest( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + let data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + let req_data = data["Request"]; + let res_data = data["Response"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + req_data, + res_data, + "no_three_ds", + "automatic", + globalState + ); + + if (should_continue) + should_continue = utils.should_continue_further(res_data); + }); + + it("payment_methods-call-test", () => { + cy.paymentMethodsCallTest(globalState); + }); + } + ); +}); diff --git a/cypress-tests/cypress/fixtures/business-profile.json b/cypress-tests/cypress/fixtures/business-profile.json new file mode 100644 index 00000000000..8d7bf637b59 --- /dev/null +++ b/cypress-tests/cypress/fixtures/business-profile.json @@ -0,0 +1,13 @@ +{ + "bpCreate": { + "profile_name": "default" + }, + + "bpUpdate": { + "is_connector_agnostic_mit_enabled": true, + "collect_shipping_details_from_wallet_connector": true, + "collect_billing_details_from_wallet_connector": true, + "always_collect_billing_details_from_wallet_connector": true, + "always_collect_shipping_details_from_wallet_connector": true + } +} diff --git a/cypress-tests/cypress/fixtures/create-business-profile.json b/cypress-tests/cypress/fixtures/create-business-profile.json deleted file mode 100644 index cdce8636157..00000000000 --- a/cypress-tests/cypress/fixtures/create-business-profile.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "profile_name": "default" -} \ No newline at end of file diff --git a/cypress-tests/cypress/fixtures/imports.js b/cypress-tests/cypress/fixtures/imports.js index 49fea00491f..58768394cbc 100644 --- a/cypress-tests/cypress/fixtures/imports.js +++ b/cypress-tests/cypress/fixtures/imports.js @@ -1,8 +1,8 @@ +import businessProfile from "./business-profile.json"; import captureBody from "./capture-flow-body.json"; import configs from "./configs.json"; import confirmBody from "./confirm-body.json"; import apiKeyCreateBody from "./create-api-key-body.json"; -import createBusinessProfile from "./create-business-profile.json"; import createConfirmPaymentBody from "./create-confirm-body.json"; import createConnectorBody from "./create-connector-body.json"; import customerCreateBody from "./create-customer-body.json"; @@ -20,7 +20,6 @@ import routingConfigBody from "./routing-config-body.json"; import saveCardConfirmBody from "./save-card-confirm-body.json"; import sessionTokenBody from "./session-token.json"; import apiKeyUpdateBody from "./update-api-key-body.json"; -import updateBusinessProfile from "./update-business-profile.json"; import updateConnectorBody from "./update-connector-body.json"; import customerUpdateBody from "./update-customer-body.json"; import voidBody from "./void-payment-body.json"; @@ -29,11 +28,11 @@ import ntidConfirmBody from "./create-ntid-mit.json"; export { apiKeyCreateBody, apiKeyUpdateBody, + businessProfile, captureBody, citConfirmBody, configs, confirmBody, - createBusinessProfile, createConfirmPaymentBody, createConnectorBody, createPaymentBody, @@ -51,7 +50,6 @@ export { routingConfigBody, saveCardConfirmBody, sessionTokenBody, - updateBusinessProfile, updateConnectorBody, voidBody, }; diff --git a/cypress-tests/cypress/fixtures/update-business-profile.json b/cypress-tests/cypress/fixtures/update-business-profile.json deleted file mode 100644 index 9d69534bae6..00000000000 --- a/cypress-tests/cypress/fixtures/update-business-profile.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "is_connector_agnostic_mit_enabled": true -} diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 17c2270efef..ee64d3247c5 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -196,9 +196,25 @@ Cypress.Commands.add( Cypress.Commands.add( "UpdateBusinessProfileTest", - (updateBusinessProfile, is_connector_agnostic_mit_enabled, globalState) => { + ( + updateBusinessProfile, + is_connector_agnostic_mit_enabled, + collect_billing_details_from_wallet_connector, + collect_shipping_details_from_wallet_connector, + always_collect_billing_details_from_wallet_connector, + always_collect_shipping_details_from_wallet_connector, + globalState + ) => { updateBusinessProfile.is_connector_agnostic_mit_enabled = is_connector_agnostic_mit_enabled; + updateBusinessProfile.collect_shipping_details_from_wallet_connector = + collect_shipping_details_from_wallet_connector; + updateBusinessProfile.collect_billing_details_from_wallet_connector = + collect_billing_details_from_wallet_connector; + updateBusinessProfile.always_collect_billing_details_from_wallet_connector = + always_collect_billing_details_from_wallet_connector; + updateBusinessProfile.always_collect_shipping_details_from_wallet_connector = + always_collect_shipping_details_from_wallet_connector; const merchant_id = globalState.get("merchantId"); const profile_id = globalState.get("profileId"); cy.request({ @@ -213,6 +229,24 @@ Cypress.Commands.add( failOnStatusCode: false, }).then((response) => { logRequestId(response.headers["x-request-id"]); + if (response.status === 200) { + globalState.set( + "collectBillingDetails", + response.body.collect_billing_details_from_wallet_connector + ); + globalState.set( + "collectShippingDetails", + response.body.collect_shipping_details_from_wallet_connector + ); + globalState.set( + "alwaysCollectBillingDetails", + response.body.always_collect_billing_details_from_wallet_connector + ); + globalState.set( + "alwaysCollectShippingDetails", + response.body.always_collect_shipping_details_from_wallet_connector + ); + } }); } ); @@ -1001,6 +1035,33 @@ Cypress.Commands.add("paymentMethodsCallTest", (globalState) => { expect(response.headers["content-type"]).to.include("application/json"); expect(response.body).to.have.property("redirect_url"); expect(response.body).to.have.property("payment_methods"); + if ( + globalState.get("collectBillingDetails") === true || + globalState.get("alwaysCollectBillingDetails") === true + ) { + expect( + response.body.collect_billing_details_from_wallets, + "collectBillingDetailsFromWallets" + ).to.be.true; + } else + expect( + response.body.collect_billing_details_from_wallets, + "collectBillingDetailsFromWallets" + ).to.be.false; + + if ( + globalState.get("collectShippingDetails") === true || + globalState.get("alwaysCollectShippingDetails") === true + ) { + expect( + response.body.collect_shipping_details_from_wallets, + "collectShippingDetailsFromWallets" + ).to.be.true; + } else + expect( + response.body.collect_shipping_details_from_wallets, + "collectShippingDetailsFromWallets" + ).to.be.false; globalState.set("paymentID", paymentIntentID); cy.log(response); }); From 93459fde5fb95f31e8f1429e806cde8e7496dd84 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:29:55 +0530 Subject: [PATCH 31/51] fix(analytics): fix bugs in payments page metrics in Analytics V2 dashboard (#6654) --- .../src/payment_intents/accumulator.rs | 10 ++- crates/analytics/src/payment_intents/core.rs | 71 +---------------- .../analytics/src/payment_intents/sankey.rs | 78 ++++++++++++------- crates/analytics/src/payments/accumulator.rs | 19 +++-- .../sessionized_metrics/failure_reasons.rs | 5 -- crates/api_models/src/analytics.rs | 18 ++--- 6 files changed, 80 insertions(+), 121 deletions(-) diff --git a/crates/analytics/src/payment_intents/accumulator.rs b/crates/analytics/src/payment_intents/accumulator.rs index ef3cd3129c4..d8f27501b56 100644 --- a/crates/analytics/src/payment_intents/accumulator.rs +++ b/crates/analytics/src/payment_intents/accumulator.rs @@ -273,8 +273,14 @@ impl PaymentIntentMetricAccumulator for PaymentsDistributionAccumulator { } } - if let Some(total) = metrics.count.and_then(|total| u32::try_from(total).ok()) { - self.total += total; + if status.as_ref() != &storage_enums::IntentStatus::RequiresCustomerAction + && status.as_ref() != &storage_enums::IntentStatus::RequiresPaymentMethod + && status.as_ref() != &storage_enums::IntentStatus::RequiresMerchantAction + && status.as_ref() != &storage_enums::IntentStatus::RequiresConfirmation + { + if let Some(total) = metrics.count.and_then(|total| u32::try_from(total).ok()) { + self.total += total; + } } } } diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs index 3654cad8c09..0b66dfda58c 100644 --- a/crates/analytics/src/payment_intents/core.rs +++ b/crates/analytics/src/payment_intents/core.rs @@ -8,10 +8,9 @@ use api_models::analytics::{ }, GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, PaymentIntentFilterValue, PaymentIntentFiltersResponse, PaymentIntentsAnalyticsMetadata, PaymentIntentsMetricsResponse, - SankeyResponse, }; use bigdecimal::ToPrimitive; -use common_enums::{Currency, IntentStatus}; +use common_enums::Currency; use common_utils::{errors::CustomResult, types::TimeRange}; use currency_conversion::{conversion::convert, types::ExchangeRates}; use error_stack::ResultExt; @@ -24,7 +23,7 @@ use router_env::{ use super::{ filters::{get_payment_intent_filter_for_dimension, PaymentIntentFilterRow}, metrics::PaymentIntentMetricRow, - sankey::{get_sankey_data, SessionizerRefundStatus}, + sankey::{get_sankey_data, SankeyRow}, PaymentIntentMetricsAccumulator, }; use crate::{ @@ -51,7 +50,7 @@ pub async fn get_sankey( pool: &AnalyticsProvider, auth: &AuthInfo, req: TimeRange, -) -> AnalyticsResult { +) -> AnalyticsResult> { match pool { AnalyticsProvider::Sqlx(_) => Err(AnalyticsError::NotImplemented( "Sankey not implemented for sqlx", @@ -62,69 +61,7 @@ pub async fn get_sankey( let sankey_rows = get_sankey_data(ckh_pool, auth, &req) .await .change_context(AnalyticsError::UnknownError)?; - let mut sankey_response = SankeyResponse::default(); - for i in sankey_rows { - match ( - i.status.as_ref(), - i.refunds_status.unwrap_or_default().as_ref(), - i.attempt_count, - ) { - (IntentStatus::Succeeded, SessionizerRefundStatus::FullRefunded, 1) => { - sankey_response.refunded += i.count; - sankey_response.normal_success += i.count - } - (IntentStatus::Succeeded, SessionizerRefundStatus::PartialRefunded, 1) => { - sankey_response.partial_refunded += i.count; - sankey_response.normal_success += i.count - } - (IntentStatus::Succeeded, SessionizerRefundStatus::FullRefunded, _) => { - sankey_response.refunded += i.count; - sankey_response.smart_retried_success += i.count - } - (IntentStatus::Succeeded, SessionizerRefundStatus::PartialRefunded, _) => { - sankey_response.partial_refunded += i.count; - sankey_response.smart_retried_success += i.count - } - ( - IntentStatus::Succeeded - | IntentStatus::PartiallyCaptured - | IntentStatus::PartiallyCapturedAndCapturable - | IntentStatus::RequiresCapture, - SessionizerRefundStatus::NotRefunded, - 1, - ) => sankey_response.normal_success += i.count, - ( - IntentStatus::Succeeded - | IntentStatus::PartiallyCaptured - | IntentStatus::PartiallyCapturedAndCapturable - | IntentStatus::RequiresCapture, - SessionizerRefundStatus::NotRefunded, - _, - ) => sankey_response.smart_retried_success += i.count, - (IntentStatus::Failed, _, 1) => sankey_response.normal_failure += i.count, - (IntentStatus::Failed, _, _) => { - sankey_response.smart_retried_failure += i.count - } - (IntentStatus::Cancelled, _, _) => sankey_response.cancelled += i.count, - (IntentStatus::Processing, _, _) => sankey_response.pending += i.count, - (IntentStatus::RequiresCustomerAction, _, _) => { - sankey_response.customer_awaited += i.count - } - (IntentStatus::RequiresMerchantAction, _, _) => { - sankey_response.merchant_awaited += i.count - } - (IntentStatus::RequiresPaymentMethod, _, _) => { - sankey_response.pm_awaited += i.count - } - (IntentStatus::RequiresConfirmation, _, _) => { - sankey_response.confirmation_awaited += i.count - } - i @ (_, _, _) => { - router_env::logger::error!(status=?i, "Unknown status in sankey data"); - } - } - } - Ok(sankey_response) + Ok(sankey_rows) } } } diff --git a/crates/analytics/src/payment_intents/sankey.rs b/crates/analytics/src/payment_intents/sankey.rs index 53fd03562f1..626dcef2744 100644 --- a/crates/analytics/src/payment_intents/sankey.rs +++ b/crates/analytics/src/payment_intents/sankey.rs @@ -5,7 +5,6 @@ use common_utils::{ }; use error_stack::ResultExt; use router_env::logger; -use time::PrimitiveDateTime; use crate::{ clickhouse::ClickhouseClient, @@ -13,29 +12,19 @@ use crate::{ types::{AnalyticsCollection, DBEnumWrapper, MetricsError, MetricsResult}, }; -#[derive(Debug, PartialEq, Eq, serde::Deserialize, Hash)] -pub struct PaymentIntentMetricRow { - pub profile_id: Option, - pub connector: Option, - pub authentication_type: Option>, - pub payment_method: Option, - pub payment_method_type: Option, - pub card_network: Option, - pub merchant_id: Option, - pub card_last_4: Option, - pub card_issuer: Option, - pub error_reason: Option, - pub first_attempt: Option, - pub total: Option, - pub count: Option, - #[serde(with = "common_utils::custom_serde::iso8601::option")] - pub start_bucket: Option, - #[serde(with = "common_utils::custom_serde::iso8601::option")] - pub end_bucket: Option, -} - #[derive( - Debug, Default, serde::Deserialize, strum::AsRefStr, strum::EnumString, strum::Display, + Clone, + Copy, + Debug, + Default, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumIter, + strum::EnumString, )] #[serde(rename_all = "snake_case")] pub enum SessionizerRefundStatus { @@ -45,13 +34,36 @@ pub enum SessionizerRefundStatus { PartialRefunded, } -#[derive(Debug, serde::Deserialize)] +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumIter, + strum::EnumString, +)] +#[serde(rename_all = "snake_case")] +pub enum SessionizerDisputeStatus { + DisputePresent, + #[default] + NotDisputed, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct SankeyRow { + pub count: i64, pub status: DBEnumWrapper, #[serde(default)] pub refunds_status: Option>, - pub attempt_count: i64, - pub count: i64, + #[serde(default)] + pub dispute_status: Option>, + pub first_attempt: i64, } impl TryInto for serde_json::Value { @@ -90,7 +102,12 @@ pub async fn get_sankey_data( .change_context(MetricsError::QueryBuildingError)?; query_builder - .add_select_column("attempt_count") + .add_select_column("dispute_status") + .attach_printable("Error adding select clause") + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_select_column("(attempt_count = 1) as first_attempt") .attach_printable("Error adding select clause") .change_context(MetricsError::QueryBuildingError)?; @@ -112,7 +129,12 @@ pub async fn get_sankey_data( .change_context(MetricsError::QueryBuildingError)?; query_builder - .add_group_by_clause("attempt_count") + .add_group_by_clause("dispute_status") + .attach_printable("Error adding group by clause") + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_group_by_clause("first_attempt") .attach_printable("Error adding group by clause") .change_context(MetricsError::QueryBuildingError)?; diff --git a/crates/analytics/src/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs index 291d7364071..20ccc634068 100644 --- a/crates/analytics/src/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -218,12 +218,19 @@ impl PaymentMetricAccumulator for PaymentsDistributionAccumulator { } } } - if let Some(total) = metrics.count.and_then(|total| u32::try_from(total).ok()) { - self.total += total; - if metrics.first_attempt.unwrap_or(false) { - self.total_without_retries += total; - } else { - self.total_with_only_retries += total; + if status.as_ref() != &storage_enums::AttemptStatus::AuthenticationFailed + && status.as_ref() != &storage_enums::AttemptStatus::PaymentMethodAwaited + && status.as_ref() != &storage_enums::AttemptStatus::DeviceDataCollectionPending + && status.as_ref() != &storage_enums::AttemptStatus::ConfirmationAwaited + && status.as_ref() != &storage_enums::AttemptStatus::Unresolved + { + if let Some(total) = metrics.count.and_then(|total| u32::try_from(total).ok()) { + self.total += total; + if metrics.first_attempt.unwrap_or(false) { + self.total_without_retries += total; + } else { + self.total_with_only_retries += total; + } } } } diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs index bcbce0502d2..c472c12795f 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs @@ -160,11 +160,6 @@ where .switch()?; } - outer_query_builder - .set_limit_by(5, &filtered_dimensions) - .attach_printable("Error adding limit clause") - .switch()?; - outer_query_builder .execute_query::(pool) .await diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index ee904652154..d25f35589b6 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -458,17 +458,9 @@ pub struct GetDisputeMetricRequest { #[derive(Clone, Debug, Default, serde::Serialize)] #[serde(rename_all = "snake_case")] pub struct SankeyResponse { - pub normal_success: i64, - pub normal_failure: i64, - pub cancelled: i64, - pub smart_retried_success: i64, - pub smart_retried_failure: i64, - pub pending: i64, - pub partial_refunded: i64, - pub refunded: i64, - pub disputed: i64, - pub pm_awaited: i64, - pub customer_awaited: i64, - pub merchant_awaited: i64, - pub confirmation_awaited: i64, + pub count: i64, + pub status: String, + pub refunds_status: Option, + pub dispute_status: Option, + pub first_attempt: i64, } From 707f48ceda789185187d23e35f483e117c67b81b Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:31:57 +0530 Subject: [PATCH 32/51] feat: add support for sdk session call in v2 (#6502) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 5 - crates/api_models/src/events/payment.rs | 1 - crates/api_models/src/payments.rs | 11 + crates/common_utils/src/macros.rs | 35 +++ crates/common_utils/src/types.rs | 2 +- .../src/merchant_connector_account.rs | 123 ++++++-- .../hyperswitch_domain_models/src/payments.rs | 3 + crates/router/src/core/payments.rs | 128 +++++++- .../src/core/payments/flows/session_flow.rs | 50 +++- crates/router/src/core/payments/operations.rs | 11 +- .../operations/payment_create_intent.rs | 1 + .../payments/operations/payment_get_intent.rs | 1 + .../payments/operations/payment_response.rs | 4 +- .../operations/payment_session_intent.rs | 281 ++++++++++++++++++ .../src/core/payments/session_operation.rs | 186 ++++++++++++ .../router/src/core/payments/transformers.rs | 182 +++++++++++- crates/router/src/routes/app.rs | 1 - crates/router/src/routes/payments.rs | 65 +++- 18 files changed, 1029 insertions(+), 61 deletions(-) create mode 100644 crates/router/src/core/payments/operations/payment_session_intent.rs create mode 100644 crates/router/src/core/payments/session_operation.rs diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 1494908c5fc..0edeb537dc1 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -15344,7 +15344,6 @@ "type": "object", "required": [ "payment_id", - "client_secret", "session_token" ], "properties": { @@ -15352,10 +15351,6 @@ "type": "string", "description": "The identifier for the payment" }, - "client_secret": { - "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" - }, "session_token": { "type": "array", "items": { diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 6fdb7d59b0f..6015682f7c3 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -402,7 +402,6 @@ impl ApiEventMetric for PaymentsManualUpdateResponse { } } -#[cfg(feature = "v1")] impl ApiEventMetric for PaymentsSessionResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payment { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c0cf816c68d..45b9bac53ac 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6084,6 +6084,7 @@ pub struct ApplepayErrorResponse { pub status_message: String, } +#[cfg(feature = "v1")] #[derive(Default, Debug, serde::Serialize, Clone, ToSchema)] pub struct PaymentsSessionResponse { /// The identifier for the payment @@ -6096,6 +6097,16 @@ pub struct PaymentsSessionResponse { pub session_token: Vec, } +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, Clone, ToSchema)] +pub struct PaymentsSessionResponse { + /// The identifier for the payment + #[schema(value_type = String)] + pub payment_id: id_type::GlobalPaymentId, + /// The list of session token object + pub session_token: Vec, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct PaymentRetrieveBody { /// The identifier for the Merchant Account. diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs index 21cec6f60fc..fe1289acba0 100644 --- a/crates/common_utils/src/macros.rs +++ b/crates/common_utils/src/macros.rs @@ -369,6 +369,41 @@ mod id_type { } } +/// Create new generic list wrapper +#[macro_export] +macro_rules! create_list_wrapper { + ( + $wrapper_name:ident, + $type_name: ty, + impl_functions: { + $($function_def: tt)* + } + ) => { + pub struct $wrapper_name(Vec<$type_name>); + impl $wrapper_name { + pub fn new(list: Vec<$type_name>) -> Self { + Self(list) + } + pub fn iter(&self) -> std::slice::Iter<'_, $type_name> { + self.0.iter() + } + $($function_def)* + } + impl Iterator for $wrapper_name { + type Item = $type_name; + fn next(&mut self) -> Option { + self.0.pop() + } + } + + impl FromIterator<$type_name> for $wrapper_name { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } + } + }; +} + /// Get the type name for a type #[macro_export] macro_rules! type_name { diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 84a70e44a32..2a271acb62b 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -845,7 +845,7 @@ mod client_secret_type { Ok(row) } } - + crate::impl_serializable_secret_id_type!(ClientSecret); #[cfg(test)] mod client_secret_tests { #![allow(clippy::expect_used)] diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index f0719ba3589..51c75113dcd 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "v2")] +use api_models::admin; +#[cfg(feature = "v2")] +use common_utils::ext_traits::ValueExt; use common_utils::{ crypto::Encryptable, date_time, @@ -9,11 +13,15 @@ use common_utils::{ use diesel_models::{enums, merchant_connector_account::MerchantConnectorAccountUpdateInternal}; use error_stack::ResultExt; use masking::{PeekInterface, Secret}; +#[cfg(feature = "v2")] +use router_env::logger; use rustc_hash::FxHashMap; use serde_json::Value; use super::behaviour; #[cfg(feature = "v2")] +use crate::errors::api_error_response::ApiErrorResponse; +#[cfg(feature = "v2")] use crate::router_data; use crate::type_encryption::{crypto_operation, CryptoOperation}; @@ -90,6 +98,27 @@ impl MerchantConnectorAccount { self.id.clone() } + pub fn get_metadata(&self) -> Option { + self.metadata.clone() + } + + pub fn get_parsed_payment_methods_enabled( + &self, + ) -> Vec> { + self.payment_methods_enabled + .clone() + .unwrap_or_default() + .into_iter() + .map(|payment_methods_enabled| { + payment_methods_enabled + .parse_value::("payment_methods_enabled") + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "payment_methods_enabled", + }) + }) + .collect() + } + pub fn is_disabled(&self) -> bool { self.disabled.unwrap_or(false) } @@ -530,31 +559,73 @@ impl From for MerchantConnectorAccountUpdateInte } } -#[derive(Debug)] -pub struct MerchantConnectorAccounts(Vec); - -impl MerchantConnectorAccounts { - pub fn new(merchant_connector_accounts: Vec) -> Self { - Self(merchant_connector_accounts) - } - - pub fn is_merchant_connector_account_id_in_connector_mandate_details( - &self, - profile_id: Option<&id_type::ProfileId>, - connector_mandate_details: &diesel_models::PaymentsMandateReference, - ) -> bool { - let mca_ids = self - .0 - .iter() - .filter(|mca| { - mca.disabled.is_some_and(|disabled| !disabled) - && profile_id.is_some_and(|profile_id| *profile_id == mca.profile_id) - }) - .map(|mca| mca.get_id()) - .collect::>(); +common_utils::create_list_wrapper!( + MerchantConnectorAccounts, + MerchantConnectorAccount, + impl_functions: { + #[cfg(feature = "v2")] + pub fn get_connector_and_supporting_payment_method_type_for_session_call( + &self, + ) -> Vec<(&MerchantConnectorAccount, common_enums::PaymentMethodType)> { + let connector_and_supporting_payment_method_type = self.iter().flat_map(|connector_account| { + connector_account + .get_parsed_payment_methods_enabled() + // TODO: make payment_methods_enabled strict type in DB + .into_iter() + .filter_map(|parsed_payment_method_result| { + parsed_payment_method_result + .inspect_err(|err| { + logger::error!(session_token_parsing_error=?err); + }) + .ok() + }) + .flat_map(|parsed_payment_methods_enabled| { + parsed_payment_methods_enabled + .payment_method_types + .unwrap_or_default() + .into_iter() + .filter(|payment_method_type| { + let is_invoke_sdk_client = matches!( + payment_method_type.payment_experience, + Some(api_models::enums::PaymentExperience::InvokeSdkClient) + ); + is_invoke_sdk_client + }) + .map(|payment_method_type| { + (connector_account, payment_method_type.payment_method_type) + }) + .collect::>() + }) + .collect::>() + }).collect(); + connector_and_supporting_payment_method_type + } + pub fn filter_based_on_profile_and_connector_type( + self, + profile_id: &id_type::ProfileId, + connector_type: common_enums::ConnectorType, + ) -> Self { + self.into_iter() + .filter(|mca| &mca.profile_id == profile_id && mca.connector_type == connector_type) + .collect() + } + pub fn is_merchant_connector_account_id_in_connector_mandate_details( + &self, + profile_id: Option<&id_type::ProfileId>, + connector_mandate_details: &diesel_models::PaymentsMandateReference, + ) -> bool { + let mca_ids = self + .iter() + .filter(|mca| { + mca.disabled.is_some_and(|disabled| !disabled) + && profile_id.is_some_and(|profile_id| *profile_id == mca.profile_id) + }) + .map(|mca| mca.get_id()) + .collect::>(); - connector_mandate_details - .keys() - .any(|mca_id| mca_ids.contains(mca_id)) + connector_mandate_details + .keys() + .any(|mca_id| mca_ids.contains(mca_id)) + } } -} +); diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 4cb93403219..b7a6c12500d 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -1,6 +1,8 @@ #[cfg(feature = "v2")] use std::marker::PhantomData; +#[cfg(feature = "v2")] +use api_models::payments::SessionToken; #[cfg(feature = "v2")] use common_utils::ext_traits::ValueExt; use common_utils::{ @@ -566,6 +568,7 @@ where { pub flow: PhantomData, pub payment_intent: PaymentIntent, + pub sessions_token: Vec, } // TODO: Check if this can be merged with existing payment data diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index ada8160c6d9..5eb97e6eebe 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -8,6 +8,8 @@ pub mod operations; #[cfg(feature = "retry")] pub mod retry; pub mod routing; +#[cfg(feature = "v2")] +pub mod session_operation; pub mod tokenization; pub mod transformers; pub mod types; @@ -56,6 +58,8 @@ use router_env::{instrument, metrics::add_attributes, tracing}; #[cfg(feature = "olap")] use router_types::transformers::ForeignFrom; use scheduler::utils as pt_utils; +#[cfg(feature = "v2")] +pub use session_operation::payments_session_core; #[cfg(feature = "olap")] use strum::IntoEnumIterator; use time; @@ -3111,6 +3115,119 @@ where } } +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn call_multiple_connectors_service( + state: &SessionState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + connectors: Vec, + _operation: &Op, + mut payment_data: D, + customer: &Option, + _session_surcharge_details: Option, + business_profile: &domain::Profile, + header_payload: HeaderPayload, +) -> RouterResult +where + Op: Debug, + F: Send + Clone, + + // To create connector flow specific interface data + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + D: ConstructFlowSpecificData, + RouterData: Feature, + + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, +{ + let call_connectors_start_time = Instant::now(); + let mut join_handlers = Vec::with_capacity(connectors.len()); + for session_connector_data in connectors.iter() { + let merchant_connector_id = session_connector_data + .connector + .merchant_connector_id + .as_ref() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("connector id is not set")?; + // TODO: make this DB call parallel + let merchant_connector_account = state + .store + .find_merchant_connector_account_by_id(&state.into(), merchant_connector_id, key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_connector_id.get_string_repr().to_owned(), + })?; + let connector_id = session_connector_data.connector.connector.id(); + let router_data = payment_data + .construct_router_data( + state, + connector_id, + merchant_account, + key_store, + customer, + &merchant_connector_account, + None, + None, + ) + .await?; + + let res = router_data.decide_flows( + state, + &session_connector_data.connector, + CallConnectorAction::Trigger, + None, + business_profile, + header_payload.clone(), + ); + + join_handlers.push(res); + } + + let result = join_all(join_handlers).await; + + for (connector_res, session_connector) in result.into_iter().zip(connectors) { + let connector_name = session_connector.connector.connector_name.to_string(); + match connector_res { + Ok(connector_response) => { + if let Ok(router_types::PaymentsResponseData::SessionResponse { + session_token, + .. + }) = connector_response.response.clone() + { + // If session token is NoSessionTokenReceived, it is not pushed into the sessions_token as there is no response or there can be some error + // In case of error, that error is already logged + if !matches!( + session_token, + api_models::payments::SessionToken::NoSessionTokenReceived, + ) { + payment_data.push_sessions_token(session_token); + } + } + if let Err(connector_error_response) = connector_response.response { + logger::error!( + "sessions_connector_error {} {:?}", + connector_name, + connector_error_response + ); + } + } + Err(api_error) => { + logger::error!("sessions_api_error {} {:?}", connector_name, api_error); + } + } + } + + let call_connectors_end_time = Instant::now(); + let call_connectors_duration = + call_connectors_end_time.saturating_duration_since(call_connectors_start_time); + tracing::info!(duration = format!("Duration taken: {}", call_connectors_duration.as_millis())); + + Ok(payment_data) +} + #[cfg(feature = "v1")] #[allow(clippy::too_many_arguments)] pub async fn call_multiple_connectors_service( @@ -3137,9 +3254,6 @@ where // To construct connector flow specific api dyn api::Connector: services::api::ConnectorIntegration, - - // To perform router related operation for PaymentResponse - PaymentResponse: Operation, { let call_connectors_start_time = Instant::now(); let mut join_handlers = Vec::with_capacity(connectors.len()); @@ -3574,6 +3688,7 @@ pub fn is_preprocessing_required_for_wallets(connector_name: String) -> bool { connector_name == *"trustpay" || connector_name == *"payme" } +#[cfg(feature = "v1")] #[instrument(skip_all)] pub async fn construct_profile_id_and_get_mca<'a, F, D>( state: &'a SessionState, @@ -3588,7 +3703,6 @@ where F: Clone, D: OperationSessionGetters + Send + Sync + Clone, { - #[cfg(feature = "v1")] let profile_id = payment_data .get_payment_intent() .profile_id @@ -6977,7 +7091,7 @@ impl OperationSessionGetters for PaymentIntentData { } fn get_sessions_token(&self) -> Vec { - todo!() + self.sessions_token.clone() } fn get_token_data(&self) -> Option<&storage::PaymentTokenData> { @@ -7033,8 +7147,8 @@ impl OperationSessionSetters for PaymentIntentData { todo!() } - fn push_sessions_token(&mut self, _token: api::SessionToken) { - todo!() + fn push_sessions_token(&mut self, token: api::SessionToken) { + self.sessions_token.push(token); } fn set_surcharge_details(&mut self, _surcharge_details: Option) { diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 43f855182e8..265046f42d9 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -6,6 +6,8 @@ use common_utils::{ types::{AmountConvertor, StringMajorUnitForConnector}, }; use error_stack::{Report, ResultExt}; +#[cfg(feature = "v2")] +use hyperswitch_domain_models::payments::PaymentIntentData; use masking::ExposeInterface; use router_env::metrics::add_attributes; @@ -26,12 +28,12 @@ use crate::{ utils::OptionExt, }; +#[cfg(feature = "v2")] #[async_trait] impl ConstructFlowSpecificData - for PaymentData + for PaymentIntentData { - #[cfg(feature = "v1")] async fn construct_router_data<'a>( &self, state: &routes::SessionState, @@ -39,14 +41,11 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, - merchant_connector_account: &helpers::MerchantConnectorAccountType, + merchant_connector_account: &domain::MerchantConnectorAccount, merchant_recipient_data: Option, header_payload: Option, ) -> RouterResult { - Box::pin(transformers::construct_payment_router_data::< - api::Session, - types::PaymentsSessionData, - >( + Box::pin(transformers::construct_payment_router_data_for_sdk_session( state, self.clone(), connector_id, @@ -60,7 +59,24 @@ impl .await } - #[cfg(feature = "v2")] + async fn get_merchant_recipient_data<'a>( + &self, + _state: &routes::SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[cfg(feature = "v1")] +#[async_trait] +impl + ConstructFlowSpecificData + for PaymentData +{ async fn construct_router_data<'a>( &self, state: &routes::SessionState, @@ -68,11 +84,25 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, - merchant_connector_account: &domain::MerchantConnectorAccount, + merchant_connector_account: &helpers::MerchantConnectorAccountType, merchant_recipient_data: Option, header_payload: Option, ) -> RouterResult { - todo!() + Box::pin(transformers::construct_payment_router_data::< + api::Session, + types::PaymentsSessionData, + >( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + )) + .await } async fn get_merchant_recipient_data<'a>( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index e936f372545..5cdfff60b45 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -17,6 +17,8 @@ pub mod payment_reject; pub mod payment_response; #[cfg(feature = "v1")] pub mod payment_session; +#[cfg(feature = "v2")] +pub mod payment_session_intent; #[cfg(feature = "v1")] pub mod payment_start; #[cfg(feature = "v1")] @@ -45,10 +47,6 @@ use async_trait::async_trait; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; -#[cfg(feature = "v2")] -pub use self::payment_confirm_intent::PaymentIntentConfirm; -#[cfg(feature = "v2")] -pub use self::payment_create_intent::PaymentIntentCreate; #[cfg(feature = "v2")] pub use self::payment_get::PaymentGet; #[cfg(feature = "v2")] @@ -64,6 +62,11 @@ pub use self::{ payments_incremental_authorization::PaymentIncrementalAuthorization, tax_calculation::PaymentSessionUpdate, }; +#[cfg(feature = "v2")] +pub use self::{ + payment_confirm_intent::PaymentIntentConfirm, payment_create_intent::PaymentIntentCreate, + payment_session_intent::PaymentSessionIntent, +}; use super::{helpers, CustomerDetails, OperationSessionGetters, OperationSessionSetters}; use crate::{ core::errors::{self, CustomResult, RouterResult}, diff --git a/crates/router/src/core/payments/operations/payment_create_intent.rs b/crates/router/src/core/payments/operations/payment_create_intent.rs index bf5b4fb80c9..988e040f376 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -158,6 +158,7 @@ impl GetTracker, PaymentsCrea let payment_data = payments::PaymentIntentData { flow: PhantomData, payment_intent, + sessions_token: vec![], }; let get_trackers_response = operations::GetTrackerResponse { payment_data }; diff --git a/crates/router/src/core/payments/operations/payment_get_intent.rs b/crates/router/src/core/payments/operations/payment_get_intent.rs index 6424ff5a2b3..344f10434bd 100644 --- a/crates/router/src/core/payments/operations/payment_get_intent.rs +++ b/crates/router/src/core/payments/operations/payment_get_intent.rs @@ -101,6 +101,7 @@ impl GetTracker, PaymentsGetI let payment_data = payments::PaymentIntentData { flow: PhantomData, payment_intent, + sessions_token: vec![], }; let get_trackers_response = operations::GetTrackerResponse { payment_data }; diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 8fb36764c97..e3f9ad5d905 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -13,7 +13,9 @@ use error_stack::{report, ResultExt}; use futures::FutureExt; use hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt; #[cfg(feature = "v2")] -use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentStatusData}; +use hyperswitch_domain_models::payments::{ + PaymentConfirmData, PaymentIntentData, PaymentStatusData, +}; use router_derive; use router_env::{instrument, logger, metrics::add_attributes, tracing}; use storage_impl::DataModelExt; diff --git a/crates/router/src/core/payments/operations/payment_session_intent.rs b/crates/router/src/core/payments/operations/payment_session_intent.rs new file mode 100644 index 00000000000..ee490408cb1 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_session_intent.rs @@ -0,0 +1,281 @@ +use std::marker::PhantomData; + +use api_models::payments::PaymentsSessionRequest; +use async_trait::async_trait; +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use router_env::{instrument, logger, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payments::{self, operations, operations::ValidateStatusForOperation}, + }, + routes::SessionState, + types::{api, domain, storage::enums}, + utils::ext_traits::OptionExt, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentSessionIntent; + +impl ValidateStatusForOperation for PaymentSessionIntent { + /// Validate if the current operation can be performed on the current status of the payment intent + fn validate_status_for_operation( + &self, + intent_status: common_enums::IntentStatus, + ) -> Result<(), errors::ApiErrorResponse> { + match intent_status { + common_enums::IntentStatus::RequiresPaymentMethod => Ok(()), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::PartiallyCapturedAndCapturable + | common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed => { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "You cannot create session token for this payment because it has status {intent_status}. Expected status is requires_payment_method.", + ), + }) + } + } + } +} + +impl Operation for &PaymentSessionIntent { + type Data = payments::PaymentIntentData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(*self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } +} + +impl Operation for PaymentSessionIntent { + type Data = payments::PaymentIntentData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&dyn Domain> { + Ok(self) + } +} + +type PaymentsCreateIntentOperation<'b, F> = + BoxedOperation<'b, F, PaymentsSessionRequest, payments::PaymentIntentData>; + +#[async_trait] +impl GetTracker, PaymentsSessionRequest> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &common_utils::id_type::GlobalPaymentId, + _request: &PaymentsSessionRequest, + merchant_account: &domain::MerchantAccount, + _profile: &domain::Profile, + key_store: &domain::MerchantKeyStore, + header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult>> { + let db = &*state.store; + let key_manager_state = &state.into(); + let storage_scheme = merchant_account.storage_scheme; + + let payment_intent = db + .find_payment_intent_by_id(key_manager_state, payment_id, key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + self.validate_status_for_operation(payment_intent.status)?; + + let client_secret = header_payload + .client_secret + .as_ref() + .get_required_value("client_secret header")?; + payment_intent.validate_client_secret(client_secret)?; + + let payment_data = payments::PaymentIntentData { + flow: PhantomData, + payment_intent, + sessions_token: vec![], + }; + + let get_trackers_response = operations::GetTrackerResponse { payment_data }; + + Ok(get_trackers_response) + } +} + +impl ValidateRequest> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &PaymentsSessionRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult { + Ok(operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }) + } +} + +#[async_trait] +impl Domain> + for PaymentSessionIntent +{ + #[instrument(skip_all)] + async fn get_customer_details<'a>( + &'a self, + state: &SessionState, + payment_data: &mut payments::PaymentIntentData, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsSessionRequest, payments::PaymentIntentData>, + Option, + ), + errors::StorageError, + > { + match payment_data.payment_intent.customer_id.clone() { + Some(id) => { + let customer = state + .store + .find_customer_by_global_id( + &state.into(), + id.get_string_repr(), + &payment_data.payment_intent.merchant_id, + merchant_key_store, + storage_scheme, + ) + .await?; + Ok((Box::new(self), Some(customer))) + } + None => Ok((Box::new(self), None)), + } + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a SessionState, + _payment_data: &mut payments::PaymentIntentData, + _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + PaymentsCreateIntentOperation<'a, F>, + Option, + Option, + )> { + Ok((Box::new(self), None, None)) + } + + async fn perform_routing<'a>( + &'a self, + merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, + state: &SessionState, + payment_data: &mut payments::PaymentIntentData, + merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + let db = &state.store; + let all_connector_accounts = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + &state.into(), + merchant_account.get_id(), + false, + merchant_key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Database error when querying for merchant connector accounts")?; + let all_connector_accounts = domain::MerchantConnectorAccounts::new(all_connector_accounts); + let profile_id = &payment_data.payment_intent.profile_id; + let filtered_connector_accounts = all_connector_accounts + .filter_based_on_profile_and_connector_type( + profile_id, + common_enums::ConnectorType::PaymentProcessor, + ); + let connector_and_supporting_payment_method_type = filtered_connector_accounts + .get_connector_and_supporting_payment_method_type_for_session_call(); + let mut session_connector_data = + Vec::with_capacity(connector_and_supporting_payment_method_type.len()); + for (merchant_connector_account, payment_method_type) in + connector_and_supporting_payment_method_type + { + let connector_type = api::GetToken::from(payment_method_type); + if let Ok(connector_data) = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &merchant_connector_account.connector_name.to_string(), + connector_type, + Some(merchant_connector_account.get_id()), + ) + .inspect_err(|err| { + logger::error!(session_token_error=?err); + }) { + let new_session_connector_data = + api::SessionConnectorData::new(payment_method_type, connector_data, None); + session_connector_data.push(new_session_connector_data) + }; + } + + Ok(api::ConnectorCallType::SessionMultiple( + session_connector_data, + )) + } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _payment_data: &mut payments::PaymentIntentData, + ) -> CustomResult { + Ok(false) + } +} + +impl From for api::GetToken { + fn from(value: api_models::enums::PaymentMethodType) -> Self { + match value { + api_models::enums::PaymentMethodType::GooglePay => Self::GpayMetadata, + api_models::enums::PaymentMethodType::ApplePay => Self::ApplePayMetadata, + api_models::enums::PaymentMethodType::SamsungPay => Self::SamsungPayMetadata, + api_models::enums::PaymentMethodType::Paypal => Self::PaypalSdkMetadata, + api_models::enums::PaymentMethodType::Paze => Self::PazeMetadata, + _ => Self::Connector, + } + } +} diff --git a/crates/router/src/core/payments/session_operation.rs b/crates/router/src/core/payments/session_operation.rs new file mode 100644 index 00000000000..d7b6ad0d345 --- /dev/null +++ b/crates/router/src/core/payments/session_operation.rs @@ -0,0 +1,186 @@ +use std::fmt::Debug; + +pub use common_enums::enums::CallConnectorAction; +use common_utils::id_type; +use error_stack::ResultExt; +pub use hyperswitch_domain_models::{ + mandates::{CustomerAcceptance, MandateData}, + payment_address::PaymentAddress, + payments::HeaderPayload, + router_data::{PaymentMethodToken, RouterData}, + router_request_types::CustomerDetails, +}; +use router_env::{instrument, tracing}; + +use crate::{ + core::{ + errors::{self, utils::StorageErrorExt, RouterResult}, + payments::{ + call_multiple_connectors_service, + flows::{ConstructFlowSpecificData, Feature}, + operations, + operations::{BoxedOperation, Operation}, + transformers, OperationSessionGetters, OperationSessionSetters, + }, + }, + errors::RouterResponse, + routes::{app::ReqState, SessionState}, + services, + types::{self as router_types, api, domain}, +}; + +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn payments_session_core( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + operation: Op, + req: Req, + payment_id: id_type::GlobalPaymentId, + call_connector_action: CallConnectorAction, + header_payload: HeaderPayload, +) -> RouterResponse +where + F: Send + Clone + Sync, + Req: Send + Sync, + FData: Send + Sync + Clone, + Op: Operation + Send + Sync + Clone, + Req: Debug, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + Res: transformers::ToResponse, + // To create connector flow specific interface data + D: ConstructFlowSpecificData, + RouterData: Feature, + + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, +{ + let (payment_data, _req, customer, connector_http_status_code, external_latency) = + payments_session_operation_core::<_, _, _, _, _>( + &state, + req_state, + merchant_account.clone(), + key_store, + profile, + operation.clone(), + req, + payment_id, + call_connector_action, + header_payload.clone(), + ) + .await?; + + Res::generate_response( + payment_data, + customer, + &state.base_url, + operation, + &state.conf.connector_request_reference_id_config, + connector_http_status_code, + external_latency, + header_payload.x_hs_latency, + &merchant_account, + ) +} + +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +#[instrument(skip_all, fields(payment_id, merchant_id))] +pub async fn payments_session_operation_core( + state: &SessionState, + _req_state: ReqState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + profile: domain::Profile, + operation: Op, + req: Req, + payment_id: id_type::GlobalPaymentId, + _call_connector_action: CallConnectorAction, + header_payload: HeaderPayload, +) -> RouterResult<(D, Req, Option, Option, Option)> +where + F: Send + Clone + Sync, + Req: Send + Sync, + Op: Operation + Send + Sync, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + + // To create connector flow specific interface data + D: ConstructFlowSpecificData, + RouterData: Feature, + + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, + FData: Send + Sync + Clone, +{ + let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation); + + let _validate_result = operation + .to_validate_request()? + .validate_request(&req, &merchant_account)?; + + let operations::GetTrackerResponse { mut payment_data } = operation + .to_get_tracker()? + .get_trackers( + state, + &payment_id, + &req, + &merchant_account, + &profile, + &key_store, + &header_payload, + ) + .await?; + + let (_operation, customer) = operation + .to_domain()? + .get_customer_details( + state, + &mut payment_data, + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Failed while fetching/creating customer")?; + + let connector = operation + .to_domain()? + .perform_routing( + &merchant_account, + &profile, + &state.clone(), + &mut payment_data, + &key_store, + ) + .await?; + + let payment_data = match connector { + api::ConnectorCallType::PreDetermined(_connector) => { + todo!() + } + api::ConnectorCallType::Retryable(_connectors) => todo!(), + api::ConnectorCallType::Skip => todo!(), + api::ConnectorCallType::SessionMultiple(connectors) => { + // todo: call surcharge manager for session token call. + Box::pin(call_multiple_connectors_service( + state, + &merchant_account, + &key_store, + connectors, + &operation, + payment_data, + &customer, + None, + &profile, + header_payload.clone(), + )) + .await? + } + }; + + Ok((payment_data, req, customer, None, None)) +} diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 32697508101..4baeade05ab 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -7,8 +7,7 @@ use api_models::payments::{ use common_enums::{Currency, RequestIncrementalAuthorization}; use common_utils::{ consts::X_HS_LATENCY, - fp_utils, - pii::Email, + fp_utils, pii, types::{self as common_utils_type, AmountConvertor, MinorUnit, StringMajorUnitForConnector}, }; use diesel_models::{ @@ -17,7 +16,7 @@ use diesel_models::{ }; use error_stack::{report, ResultExt}; #[cfg(feature = "v2")] -use hyperswitch_domain_models::payments::PaymentConfirmData; +use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; #[cfg(feature = "v2")] use hyperswitch_domain_models::ApiModelToDieselModelConvertor; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; @@ -510,6 +509,152 @@ pub async fn construct_router_data_for_psync<'a>( Ok(router_data) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn construct_payment_router_data_for_sdk_session<'a>( + _state: &'a SessionState, + payment_data: PaymentIntentData, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &'a Option, + merchant_connector_account: &domain::MerchantConnectorAccount, + _merchant_recipient_data: Option, + header_payload: Option, +) -> RouterResult { + fp_utils::when(merchant_connector_account.is_disabled(), || { + Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) + })?; + + let auth_type: types::ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + // TODO: Take Globalid and convert to connector reference id + let customer_id = customer + .to_owned() + .map(|customer| customer.id.clone()) + .map(std::borrow::Cow::Owned) + .map(common_utils::id_type::CustomerId::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Invalid global customer generated, not able to convert to reference id", + )?; + let email = customer + .as_ref() + .and_then(|customer| customer.email.clone()) + .map(pii::Email::from); + let order_details = payment_data + .payment_intent + .order_details + .clone() + .map(|order_details| { + order_details + .into_iter() + .map(|order_detail| order_detail.expose()) + .collect() + }); + // TODO: few fields are repeated in both routerdata and request + let request = types::PaymentsSessionData { + amount: payment_data + .payment_intent + .amount_details + .order_amount + .get_amount_as_i64(), + currency: payment_data.payment_intent.amount_details.currency, + country: payment_data + .payment_intent + .billing_address + .and_then(|billing_address| { + billing_address + .get_inner() + .address + .as_ref() + .and_then(|address| address.country) + }), + // TODO: populate surcharge here + surcharge_details: None, + order_details, + email, + minor_amount: payment_data.payment_intent.amount_details.order_amount, + }; + + // TODO: evaluate the fields in router data, if they are required or not + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.get_id().clone(), + // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. + customer_id, + connector: connector_id.to_owned(), + // TODO: evaluate why we need payment id at the connector level. We already have connector reference id + payment_id: payment_data.payment_intent.id.get_string_repr().to_owned(), + // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id + attempt_id: "".to_string(), + status: enums::AttemptStatus::Started, + payment_method: enums::PaymentMethod::Wallet, + connector_auth_type: auth_type, + description: payment_data + .payment_intent + .description + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: evaluate why we need to send merchant's return url here + // This should be the return url of application, since application takes care of the redirection + return_url: payment_data + .payment_intent + .return_url + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: Create unified address + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: payment_data.payment_intent.authentication_type, + connector_meta_data: merchant_connector_account.get_metadata(), + connector_wallets_details: None, + request, + response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), + amount_captured: None, + minor_amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_status: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + // TODO: This has to be generated as the reference id based on the connector configuration + // Some connectros might not accept accept the global id. This has to be done when generating the reference id + connector_request_reference_id: "".to_string(), + preprocessing_id: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + // TODO: take this based on the env + test_mode: Some(true), + payment_method_balance: None, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload, + connector_mandate_request_reference_id: None, + psd2_sca_exemption_type: None, + }; + + Ok(router_data) +} + #[cfg(feature = "v2")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -852,6 +997,35 @@ where } } +#[cfg(feature = "v2")] +impl ToResponse for api::PaymentsSessionResponse +where + F: Clone, + Op: Debug, + D: OperationSessionGetters, +{ + #[allow(clippy::too_many_arguments)] + fn generate_response( + payment_data: D, + _customer: Option, + _base_url: &str, + _operation: Op, + _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, + _connector_http_status_code: Option, + _external_latency: Option, + _is_latency_header_enabled: Option, + _merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + Ok(services::ApplicationResponse::JsonWithHeaders(( + Self { + session_token: payment_data.get_sessions_token(), + payment_id: payment_data.get_payment_intent().id.clone(), + }, + vec![], + ))) + } +} + #[cfg(feature = "v1")] impl ToResponse for api::PaymentsDynamicTaxCalculationResponse where @@ -1503,7 +1677,7 @@ where .and_then(|customer_data| customer_data.email.clone()) .or(customer_details_encrypted_data.email.or(customer .as_ref() - .and_then(|customer| customer.email.clone().map(Email::from)))), + .and_then(|customer| customer.email.clone().map(pii::Email::from)))), phone: customer_table_response .as_ref() .and_then(|customer_data| customer_data.phone.clone()) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 1584cfae2b9..0f13e08d53e 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1676,7 +1676,6 @@ impl PayoutLink { route } } - pub struct Profile; #[cfg(all(feature = "olap", feature = "v2"))] impl Profile { diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 48b15c3b204..93f50f3ed9a 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -695,8 +695,61 @@ pub async fn payments_connector_session( state: web::Data, req: actix_web::HttpRequest, json_payload: web::Json, + path: web::Path, ) -> impl Responder { - "Session Response" + use hyperswitch_domain_models::payments::PaymentIntentData; + let flow = Flow::PaymentsSessionToken; + + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id, + payload: json_payload.into_inner(), + }; + + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, req_state| { + let payment_id = req.global_payment_id; + let request = req.payload; + let operation = payments::operations::PaymentSessionIntent; + payments::payments_session_core::< + api_types::Session, + payment_types::PaymentsSessionResponse, + _, + _, + _, + PaymentIntentData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + operation, + request, + payment_id, + payments::CallConnectorAction::Trigger, + header_payload.clone(), + ) + }, + &auth::HeaderAuth(auth::PublishableKeyAuth), + locking_action, + )) + .await } #[cfg(feature = "v1")] @@ -1786,6 +1839,16 @@ impl GetLockingInput for payment_types::PaymentsSessionRequest { } } +#[cfg(feature = "v2")] +impl GetLockingInput for payment_types::PaymentsSessionRequest { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + { + api_locking::LockAction::NotApplicable + } +} + #[cfg(feature = "v1")] impl GetLockingInput for payment_types::PaymentsDynamicTaxCalculationRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction From 5a98ed65a94a6e8204a3ea34f834033654fdbaa7 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 28 Nov 2024 19:53:20 +0530 Subject: [PATCH 33/51] feat(connector): worldpay - add dynamic fields and update terminal status mapping (#6468) --- .../src/query/payment_attempt.rs | 4 +- .../src/connectors/worldpay.rs | 4 +- .../src/connectors/worldpay/transformers.rs | 32 +- .../src/payments/payment_attempt.rs | 2 +- .../payment_connector_required_fields.rs | 284 +++++++++++--- crates/router/src/core/refunds.rs | 6 +- crates/router/src/db/kafka_store.rs | 2 +- .../src/mock_db/payment_attempt.rs | 2 +- .../src/payments/payment_attempt.rs | 9 +- .../cypress/e2e/PaymentUtils/WorldPay.js | 367 ++++++++++++++---- .../cypress/fixtures/confirm-body.json | 2 +- .../cypress/fixtures/create-confirm-body.json | 2 +- .../cypress/fixtures/create-mandate-cit.json | 2 +- .../cypress/fixtures/create-mandate-mit.json | 2 +- .../cypress/fixtures/create-pm-id-mit.json | 2 +- .../fixtures/save-card-confirm-body.json | 2 +- cypress-tests/cypress/support/commands.js | 16 +- 17 files changed, 564 insertions(+), 176 deletions(-) diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index 46a84488898..9dd13e5b030 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -97,14 +97,14 @@ impl PaymentAttempt { #[cfg(feature = "v1")] pub async fn find_by_connector_transaction_id_payment_id_merchant_id( conn: &PgPooledConn, - connector_transaction_id: &str, + connector_transaction_id: &common_utils::types::ConnectorTransactionId, payment_id: &common_utils::id_type::PaymentId, merchant_id: &common_utils::id_type::MerchantId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, dsl::connector_transaction_id - .eq(connector_transaction_id.to_owned()) + .eq(connector_transaction_id.get_id().to_owned()) .and(dsl::payment_id.eq(payment_id.to_owned())) .and(dsl::merchant_id.eq(merchant_id.to_owned())), ) diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay.rs b/crates/hyperswitch_connectors/src/connectors/worldpay.rs index fd67bb60128..be8f9bca352 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay.rs @@ -617,7 +617,7 @@ impl ConnectorIntegration fo .map(|id| id.to_string()) }); Ok(PaymentsCaptureRouterData { - status: enums::AttemptStatus::Pending, + status: enums::AttemptStatus::from(response.outcome.clone()), response: Ok(PaymentsResponseData::TransactionResponse { resource_id: ResponseId::foreign_try_from(( response, @@ -967,12 +967,12 @@ impl ConnectorIntegration for Worldpa }); Ok(RefundExecuteRouterData { response: Ok(RefundsResponseData { + refund_status: enums::RefundStatus::from(response.outcome.clone()), connector_refund_id: ResponseIdStr::foreign_try_from(( response, optional_correlation_id, ))? .id, - refund_status: enums::RefundStatus::Pending, }), ..data.clone() }) diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index 757760965e7..8ecc3ad792c 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -560,7 +560,7 @@ impl From for enums::AttemptStatus { fn from(item: PaymentOutcome) -> Self { match item { PaymentOutcome::Authorized => Self::Authorized, - PaymentOutcome::SentForSettlement => Self::CaptureInitiated, + PaymentOutcome::SentForSettlement => Self::Charged, PaymentOutcome::ThreeDsDeviceDataRequired => Self::DeviceDataCollectionPending, PaymentOutcome::ThreeDsAuthenticationFailed => Self::AuthenticationFailed, PaymentOutcome::ThreeDsChallenged => Self::AuthenticationPending, @@ -574,20 +574,38 @@ impl From for enums::AttemptStatus { } } +impl From for enums::RefundStatus { + fn from(item: PaymentOutcome) -> Self { + match item { + PaymentOutcome::SentForPartialRefund | PaymentOutcome::SentForRefund => Self::Success, + PaymentOutcome::Refused + | PaymentOutcome::FraudHighRisk + | PaymentOutcome::Authorized + | PaymentOutcome::SentForSettlement + | PaymentOutcome::ThreeDsDeviceDataRequired + | PaymentOutcome::ThreeDsAuthenticationFailed + | PaymentOutcome::ThreeDsChallenged + | PaymentOutcome::SentForCancellation + | PaymentOutcome::ThreeDsUnavailable => Self::Failure, + } + } +} + impl From<&EventType> for enums::AttemptStatus { fn from(value: &EventType) -> Self { match value { EventType::SentForAuthorization => Self::Authorizing, - EventType::SentForSettlement => Self::CaptureInitiated, + EventType::SentForSettlement => Self::Charged, EventType::Settled => Self::Charged, EventType::Authorized => Self::Authorized, - EventType::Refused | EventType::SettlementFailed => Self::Failure, - EventType::Cancelled - | EventType::SentForRefund + EventType::Refused + | EventType::SettlementFailed + | EventType::Expired + | EventType::Cancelled + | EventType::Error => Self::Failure, + EventType::SentForRefund | EventType::RefundFailed | EventType::Refunded - | EventType::Error - | EventType::Expired | EventType::Unknown => Self::Pending, } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 4ca6084c958..289b1bab37d 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -81,7 +81,7 @@ pub trait PaymentAttemptInterface { #[cfg(feature = "v1")] async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( &self, - connector_transaction_id: &str, + connector_transaction_id: &ConnectorTransactionId, payment_id: &id_type::PaymentId, merchant_id: &id_type::MerchantId, storage_scheme: storage_enums::MerchantStorageScheme, diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 4e80ccf027e..d359335bf12 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -3132,35 +3132,39 @@ impl Default for settings::RequiredFields { enums::Connector::Worldpay, RequiredFieldFinal { mandate: HashMap::new(), - non_mandate: HashMap::from([ - ( - "payment_method_data.card.card_number".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_number".to_string(), - display_name: "card_number".to_string(), - field_type: enums::FieldType::UserCardNumber, - value: None, - } - ), - ( - "payment_method_data.card.card_exp_month".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_exp_month".to_string(), - display_name: "card_exp_month".to_string(), - field_type: enums::FieldType::UserCardExpiryMonth, - value: None, - } - ), - ( - "payment_method_data.card.card_exp_year".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_exp_year".to_string(), - display_name: "card_exp_year".to_string(), - field_type: enums::FieldType::UserCardExpiryYear, - value: None, - } - ) - ]), + non_mandate: { + let mut pmd_fields = HashMap::from([ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ) + ]); + pmd_fields.extend(get_worldpay_billing_required_fields()); + pmd_fields + }, common: HashMap::new(), } ), @@ -6324,35 +6328,39 @@ impl Default for settings::RequiredFields { enums::Connector::Worldpay, RequiredFieldFinal { mandate: HashMap::new(), - non_mandate: HashMap::from([ - ( - "payment_method_data.card.card_number".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_number".to_string(), - display_name: "card_number".to_string(), - field_type: enums::FieldType::UserCardNumber, - value: None, - } - ), - ( - "payment_method_data.card.card_exp_month".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_exp_month".to_string(), - display_name: "card_exp_month".to_string(), - field_type: enums::FieldType::UserCardExpiryMonth, - value: None, - } - ), - ( - "payment_method_data.card.card_exp_year".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_exp_year".to_string(), - display_name: "card_exp_year".to_string(), - field_type: enums::FieldType::UserCardExpiryYear, - value: None, - } - ) - ]), + non_mandate: { + let mut pmd_fields = HashMap::from([ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ) + ]); + pmd_fields.extend(get_worldpay_billing_required_fields()); + pmd_fields + }, common: HashMap::new(), } ), @@ -12736,3 +12744,163 @@ impl Default for settings::RequiredFields { ])) } } + +pub fn get_worldpay_billing_required_fields() -> HashMap { + HashMap::from([ + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + }, + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry { + options: vec![ + "AF".to_string(), + "AU".to_string(), + "AW".to_string(), + "AZ".to_string(), + "BS".to_string(), + "BH".to_string(), + "BD".to_string(), + "BB".to_string(), + "BZ".to_string(), + "BM".to_string(), + "BT".to_string(), + "BO".to_string(), + "BA".to_string(), + "BW".to_string(), + "BR".to_string(), + "BN".to_string(), + "BG".to_string(), + "BI".to_string(), + "KH".to_string(), + "CA".to_string(), + "CV".to_string(), + "KY".to_string(), + "CL".to_string(), + "CO".to_string(), + "KM".to_string(), + "CD".to_string(), + "CR".to_string(), + "CZ".to_string(), + "DZ".to_string(), + "DK".to_string(), + "DJ".to_string(), + "ST".to_string(), + "DO".to_string(), + "EC".to_string(), + "EG".to_string(), + "SV".to_string(), + "ER".to_string(), + "ET".to_string(), + "FK".to_string(), + "FJ".to_string(), + "GM".to_string(), + "GE".to_string(), + "GH".to_string(), + "GI".to_string(), + "GT".to_string(), + "GN".to_string(), + "GY".to_string(), + "HT".to_string(), + "HN".to_string(), + "HK".to_string(), + "HU".to_string(), + "IS".to_string(), + "IN".to_string(), + "ID".to_string(), + "IR".to_string(), + "IQ".to_string(), + "IE".to_string(), + "IL".to_string(), + "IT".to_string(), + "JM".to_string(), + "JP".to_string(), + "JO".to_string(), + "KZ".to_string(), + "KE".to_string(), + "KW".to_string(), + "LA".to_string(), + "LB".to_string(), + "LS".to_string(), + "LR".to_string(), + "LY".to_string(), + "LT".to_string(), + "MO".to_string(), + "MK".to_string(), + "MG".to_string(), + "MW".to_string(), + "MY".to_string(), + "MV".to_string(), + "MR".to_string(), + "MU".to_string(), + "MX".to_string(), + "MD".to_string(), + "MN".to_string(), + "MA".to_string(), + "MZ".to_string(), + "MM".to_string(), + "NA".to_string(), + "NZ".to_string(), + "NI".to_string(), + "NG".to_string(), + "KP".to_string(), + "NO".to_string(), + "AR".to_string(), + "PK".to_string(), + "PG".to_string(), + "PY".to_string(), + "PE".to_string(), + "UY".to_string(), + "PH".to_string(), + "PL".to_string(), + "GB".to_string(), + "QA".to_string(), + "OM".to_string(), + "RO".to_string(), + "RU".to_string(), + "RW".to_string(), + "WS".to_string(), + "SG".to_string(), + "ST".to_string(), + "ZA".to_string(), + "KR".to_string(), + "LK".to_string(), + "SH".to_string(), + "SD".to_string(), + "SR".to_string(), + "SZ".to_string(), + "SE".to_string(), + "CH".to_string(), + "SY".to_string(), + "TW".to_string(), + "TJ".to_string(), + "TZ".to_string(), + "TH".to_string(), + "TT".to_string(), + "TN".to_string(), + "TR".to_string(), + "UG".to_string(), + "UA".to_string(), + "US".to_string(), + "UZ".to_string(), + "VU".to_string(), + "VE".to_string(), + "VN".to_string(), + "ZM".to_string(), + "ZW".to_string(), + ], + }, + value: None, + }, + ), + ]) +} diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index c3e49a49bb1..c706d8854af 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use api_models::admin::MerchantConnectorInfo; use common_utils::{ ext_traits::{AsyncExt, ValueExt}, - types::{ConnectorTransactionId, ConnectorTransactionIdTrait, MinorUnit}, + types::{ConnectorTransactionId, MinorUnit}, }; use diesel_models::process_tracker::business_status; use error_stack::{report, ResultExt}; @@ -446,7 +446,7 @@ pub async fn refund_retrieve_core( let payment_attempt = db .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( - refund.get_connector_transaction_id(), + &refund.connector_transaction_id, payment_id, merchant_id, merchant_account.storage_scheme, @@ -1451,7 +1451,7 @@ pub async fn trigger_refund_execute_workflow( let payment_attempt = db .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( - refund.get_connector_transaction_id(), + &refund.connector_transaction_id, &refund_core.payment_id, &refund.merchant_id, merchant_account.storage_scheme, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index a9df1abbc08..e37ff3aa167 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1454,7 +1454,7 @@ impl PaymentAttemptInterface for KafkaStore { #[cfg(feature = "v1")] async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( &self, - connector_transaction_id: &str, + connector_transaction_id: &common_utils::types::ConnectorTransactionId, payment_id: &id_type::PaymentId, merchant_id: &id_type::MerchantId, storage_scheme: MerchantStorageScheme, diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index 83691f46129..0415625f7eb 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -254,7 +254,7 @@ impl PaymentAttemptInterface for MockDb { #[cfg(feature = "v1")] async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( &self, - _connector_transaction_id: &str, + _connector_transaction_id: &common_utils::types::ConnectorTransactionId, _payment_id: &common_utils::id_type::PaymentId, _merchant_id: &common_utils::id_type::MerchantId, _storage_scheme: storage_enums::MerchantStorageScheme, diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 9b9121952b1..4eedfd5005e 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -151,7 +151,7 @@ impl PaymentAttemptInterface for RouterStore { #[instrument(skip_all)] async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( &self, - connector_transaction_id: &str, + connector_transaction_id: &ConnectorTransactionId, payment_id: &common_utils::id_type::PaymentId, merchant_id: &common_utils::id_type::MerchantId, _storage_scheme: MerchantStorageScheme, @@ -786,7 +786,7 @@ impl PaymentAttemptInterface for KVRouterStore { #[instrument(skip_all)] async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( &self, - connector_transaction_id: &str, + connector_transaction_id: &ConnectorTransactionId, payment_id: &common_utils::id_type::PaymentId, merchant_id: &common_utils::id_type::MerchantId, storage_scheme: MerchantStorageScheme, @@ -811,8 +811,9 @@ impl PaymentAttemptInterface for KVRouterStore { MerchantStorageScheme::RedisKv => { // We assume that PaymentAttempt <=> PaymentIntent is a one-to-one relation for now let lookup_id = format!( - "pa_conn_trans_{}_{connector_transaction_id}", - merchant_id.get_string_repr() + "pa_conn_trans_{}_{}", + merchant_id.get_string_repr(), + connector_transaction_id.get_id() ); let lookup = fallback_reverse_lookup_not_found!( self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) diff --git a/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js b/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js index 5b950219fb2..d45458369c5 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/WorldPay.js @@ -78,15 +78,17 @@ const payment_method_data_3ds = { billing: null }; -const singleUseMandateData = { - customer_acceptance: { - acceptance_type: "offline", - accepted_at: "1963-05-03T04:07:52.723Z", - online: { - ip_address: "125.0.0.1", - user_agent: "amet irure esse", - }, +const offileCustomerAcceptance = { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", }, +}; + +const singleUseMandateData = { + customer_acceptance: offileCustomerAcceptance, mandate_type: { single_use: { amount: 8000, @@ -102,7 +104,8 @@ export const connectorDetails = { currency: "USD", customer_acceptance: null, setup_future_usage: "on_session", - }, Response: { + }, + Response: { status: 200, body: { status: "requires_payment_method", @@ -117,7 +120,6 @@ export const connectorDetails = { payment_method_data: { card: successfulNoThreeDsCardDetailsRequest, }, - currency: "USD", customer_acceptance: null, setup_future_usage: "on_session", billing: billing, @@ -127,7 +129,7 @@ export const connectorDetails = { body: { status: "requires_capture", payment_method: "card", - payment_method_type: "debit", + payment_method_type: "credit", attempt_count: 1, payment_method_data: paymentMethodDataNoThreeDsResponse, }, @@ -140,16 +142,15 @@ export const connectorDetails = { payment_method_data: { card: successfulNoThreeDsCardDetailsRequest, }, - currency: "USD", customer_acceptance: null, setup_future_usage: "on_session", }, Response: { status: 200, body: { - status: "processing", + status: "succeeded", payment_method: "card", - payment_method_type: "debit", + payment_method_type: "credit", attempt_count: 1, payment_method_data: paymentMethodDataNoThreeDsResponse, }, @@ -168,9 +169,9 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "processing", + status: "succeeded", amount: 6500, - amount_capturable: 6500, + amount_capturable: 0, }, }, }, @@ -187,9 +188,9 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "processing", + status: "partially_captured", amount: 6500, - amount_capturable: 6500, + amount_capturable: 0, }, }, }, @@ -226,23 +227,38 @@ export const connectorDetails = { }, currency: "USD", setup_future_usage: "on_session", - customer_acceptance: { - acceptance_type: "offline", - accepted_at: "1963-05-03T04:07:52.723Z", - online: { - ip_address: "127.0.0.1", - user_agent: "amet irure esse", - }, + customer_acceptance: offileCustomerAcceptance, + }, + Response: { + body: { + status: "requires_capture", + }, + }, + }, + SaveCardUseNo3DSManualCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, }, + setup_future_usage: "off_session", + customer_acceptance: offileCustomerAcceptance, }, Response: { - status: 400, + status: 200, body: { - error: { - type: "invalid_request", - message: "Missing required param: payment_method_data", - code: "IR_04" - } + status: "requires_capture", + }, + }, + }, + SaveCardConfirmManualCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", }, }, }, @@ -255,22 +271,43 @@ export const connectorDetails = { currency: "USD", setup_future_usage: "on_session", browser_info, - customer_acceptance: { - acceptance_type: "offline", - accepted_at: "1963-05-03T04:07:52.723Z", - online: { - ip_address: "127.0.0.1", - user_agent: "amet irure esse", - }, - }, + customer_acceptance: offileCustomerAcceptance, }, Response: { status: 200, body: { - status: "processing" + status: "succeeded" }, } }, + SaveCardUseNo3DSAutoCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_type: "debit", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + setup_future_usage: "off_session", + customer_acceptance: offileCustomerAcceptance, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardConfirmAutoCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, "3DSManualCapture": { Request: { payment_method: "card", @@ -278,7 +315,6 @@ export const connectorDetails = { payment_method_data: { card: successfulThreeDsTestCardDetailsRequest, }, - currency: "USD", customer_acceptance: null, setup_future_usage: "on_session", browser_info, @@ -313,10 +349,6 @@ export const connectorDetails = { }, }, }, - - /** - * Variation cases - */ CaptureCapturedAmount: { Request: { Request: { @@ -334,7 +366,7 @@ export const connectorDetails = { error: { type: "invalid_request", message: - "This Payment could not be captured because it has a capture_method of automatic. The expected state is manual_multiple", + "This Payment could not be captured because it has a payment.status of succeeded. The expected state is requires_capture, partially_captured_and_capturable, processing", code: "IR_14", }, }, @@ -346,7 +378,6 @@ export const connectorDetails = { payment_method_data: { card: successfulNoThreeDsCardDetailsRequest, }, - currency: "USD", customer_acceptance: null, }, Response: { @@ -355,37 +386,17 @@ export const connectorDetails = { error: { type: "invalid_request", message: - "You cannot confirm this payment because it has status processing", + "You cannot confirm this payment because it has status succeeded", code: "IR_16", }, }, }, }, - - /** - * Not implemented or not ready for running test cases - * - Refunds - * - Mandates - */ Refund: { Request: {}, Response: { body: { - error: { - type: "invalid_request", - message: "This Payment could not be refund because it has a status of processing. The expected state is succeeded, partially_captured", - code: "IR_14" - } - } - }, - ResponseCustom: { - status: 400, - body: { - error: { - type: "invalid_request", - message: "This Payment could not be refund because it has a status of processing. The expected state is succeeded, partially_captured", - code: "IR_14", - }, + status: "succeeded" }, }, }, @@ -393,11 +404,35 @@ export const connectorDetails = { Request: {}, Response: { body: { - error: { - type: "invalid_request", - message: "This Payment could not be refund because it has a status of processing. The expected state is succeeded, partially_captured", - code: "IR_14" - } + status: "succeeded" + } + } + }, + manualPaymentRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + }, + Response: { + body: { + status: "succeeded" + } + } + }, + manualPaymentPartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + }, + Response: { + body: { + status: "succeeded" } } }, @@ -405,14 +440,74 @@ export const connectorDetails = { Request: {}, Response: { body: { - error: { - type: "invalid_request", - message: "Refund does not exist in our records.", - code: "HE_02" - } + status: "succeeded" } } }, + MandateSingleUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateSingleUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MandateMultiUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, ZeroAuthMandate: { Request: { payment_method: "card", @@ -423,12 +518,116 @@ export const connectorDetails = { mandate_data: singleUseMandateData, }, Response: { + trigger_skip: true, + status: 200, body: { - error: { - type: "invalid_request", - message: "Setup Mandate flow for Worldpay is not implemented", - code: "IR_00" - } + error_code: "internalErrorOccurred", + error_message: "We cannot currently process your request. Please contact support.", + status: "failed", + payment_method_id: null + }, + }, + }, + ZeroAuthPaymentIntent: { + Request: { + amount: 0, + setup_future_usage: "off_session", + currency: "USD", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", + }, + }, + }, + ZeroAuthConfirmPayment: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + trigger_skip: true, + status: 200, + body: { + error_code: "internalErrorOccurred", + error_message: "We cannot currently process your request. Please contact support.", + status: "failed", + payment_method_id: null + }, + }, + }, + PaymentMethodIdMandateNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + mandate_data: null, + customer_acceptance: offileCustomerAcceptance, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNoThreeDsCardDetailsRequest, + }, + currency: "USD", + mandate_data: null, + customer_acceptance: offileCustomerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + PaymentMethodIdMandate3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDsTestCardDetailsRequest, + }, + currency: "USD", + mandate_data: null, + authentication_type: "three_ds", + customer_acceptance: offileCustomerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", + }, + }, + }, + PaymentMethodIdMandate3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulThreeDsTestCardDetailsRequest, + }, + mandate_data: null, + authentication_type: "three_ds", + customer_acceptance: offileCustomerAcceptance, + }, + Response: { + status: 200, + body: { + status: "requires_customer_action", }, }, }, diff --git a/cypress-tests/cypress/fixtures/confirm-body.json b/cypress-tests/cypress/fixtures/confirm-body.json index fa4769b627f..d92be2d91e7 100644 --- a/cypress-tests/cypress/fixtures/confirm-body.json +++ b/cypress-tests/cypress/fixtures/confirm-body.json @@ -29,7 +29,7 @@ "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "language": "en-US", - "color_depth": 30, + "color_depth": 32, "screen_height": 1117, "screen_width": 1728, "time_zone": -330, diff --git a/cypress-tests/cypress/fixtures/create-confirm-body.json b/cypress-tests/cypress/fixtures/create-confirm-body.json index a779557ed5e..2a9e8b19ee1 100644 --- a/cypress-tests/cypress/fixtures/create-confirm-body.json +++ b/cypress-tests/cypress/fixtures/create-confirm-body.json @@ -70,7 +70,7 @@ "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "language": "en-US", - "color_depth": 30, + "color_depth": 32, "screen_height": 1117, "screen_width": 1728, "time_zone": -330, diff --git a/cypress-tests/cypress/fixtures/create-mandate-cit.json b/cypress-tests/cypress/fixtures/create-mandate-cit.json index c96284ea99b..d33cb8b91c7 100644 --- a/cypress-tests/cypress/fixtures/create-mandate-cit.json +++ b/cypress-tests/cypress/fixtures/create-mandate-cit.json @@ -80,7 +80,7 @@ "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "language": "en-US", - "color_depth": 30, + "color_depth": 32, "screen_height": 1117, "screen_width": 1728, "time_zone": -330, diff --git a/cypress-tests/cypress/fixtures/create-mandate-mit.json b/cypress-tests/cypress/fixtures/create-mandate-mit.json index 9612eac3209..7b70279979a 100644 --- a/cypress-tests/cypress/fixtures/create-mandate-mit.json +++ b/cypress-tests/cypress/fixtures/create-mandate-mit.json @@ -26,7 +26,7 @@ "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "language": "en-US", - "color_depth": 30, + "color_depth": 32, "screen_height": 1117, "screen_width": 1728, "time_zone": -330, diff --git a/cypress-tests/cypress/fixtures/create-pm-id-mit.json b/cypress-tests/cypress/fixtures/create-pm-id-mit.json index c78cf2a74c5..77d3c76b8b2 100644 --- a/cypress-tests/cypress/fixtures/create-pm-id-mit.json +++ b/cypress-tests/cypress/fixtures/create-pm-id-mit.json @@ -31,7 +31,7 @@ "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "language": "en-US", - "color_depth": 30, + "color_depth": 32, "screen_height": 1117, "screen_width": 1728, "time_zone": -330, diff --git a/cypress-tests/cypress/fixtures/save-card-confirm-body.json b/cypress-tests/cypress/fixtures/save-card-confirm-body.json index 17a860fd188..615cec8abf7 100644 --- a/cypress-tests/cypress/fixtures/save-card-confirm-body.json +++ b/cypress-tests/cypress/fixtures/save-card-confirm-body.json @@ -32,7 +32,7 @@ "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "language": "en-US", - "color_depth": 30, + "color_depth": 32, "screen_height": 1117, "screen_width": 1728, "time_zone": -330, diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index ee64d3247c5..5a6e1b4783e 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -1793,13 +1793,13 @@ Cypress.Commands.add( for (const key in response.body.attempts) { if ( response.body.attempts[key].attempt_id === - `${payment_id}_${attempt}` && + `${payment_id}_${attempt}` && response.body.status === "succeeded" ) { expect(response.body.attempts[key].status).to.equal("charged"); } else if ( response.body.attempts[key].attempt_id === - `${payment_id}_${attempt}` && + `${payment_id}_${attempt}` && response.body.status === "requires_customer_action" ) { expect(response.body.attempts[key].status).to.equal( @@ -1915,8 +1915,10 @@ Cypress.Commands.add( ); expect(response.body.customer, "customer").to.not.be.empty; expect(response.body.profile_id, "profile_id").to.not.be.null; - expect(response.body.payment_method_id, "payment_method_id").to.not.be - .null; + if (response.body.status !== "failed") { + expect(response.body.payment_method_id, "payment_method_id").to.not.be + .null; + } if (requestBody.mandate_data === null) { expect(response.body).to.have.property("payment_method_id"); @@ -2195,11 +2197,11 @@ Cypress.Commands.add( if (globalState.get("connectorId") !== "cybersource") { return; } - + const apiKey = globalState.get("apiKey"); const baseUrl = globalState.get("baseUrl"); const url = `${baseUrl}/payments`; - + cy.request({ method: "POST", url: url, @@ -2211,7 +2213,7 @@ Cypress.Commands.add( body: requestBody, }).then((response) => { logRequestId(response.headers["x-request-id"]); - + if (response.status === 200) { expect(response.headers["content-type"]).to.include("application/json"); From 60bc7d89774b7f7a3463eb4e29ec54e4a7163f25 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 00:22:09 +0000 Subject: [PATCH 34/51] chore(version): 2024.11.29.0 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd80af8cf7a..a215cbe0557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.11.29.0 + +### Features + +- **connector:** Worldpay - add dynamic fields and update terminal status mapping ([#6468](https://github.com/juspay/hyperswitch/pull/6468)) ([`5a98ed6`](https://github.com/juspay/hyperswitch/commit/5a98ed65a94a6e8204a3ea34f834033654fdbaa7)) +- Add support for sdk session call in v2 ([#6502](https://github.com/juspay/hyperswitch/pull/6502)) ([`707f48c`](https://github.com/juspay/hyperswitch/commit/707f48ceda789185187d23e35f483e117c67b81b)) + +### Bug Fixes + +- **analytics:** Fix bugs in payments page metrics in Analytics V2 dashboard ([#6654](https://github.com/juspay/hyperswitch/pull/6654)) ([`93459fd`](https://github.com/juspay/hyperswitch/commit/93459fde5fb95f31e8f1429e806cde8e7496dd84)) + +**Full Changelog:** [`2024.11.28.0...2024.11.29.0`](https://github.com/juspay/hyperswitch/compare/2024.11.28.0...2024.11.29.0) + +- - - + ## 2024.11.28.0 ### Bug Fixes From abcaa539eccdae86c7a68fd4ce60ab9889f9fb43 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:16:16 +0530 Subject: [PATCH 35/51] fix(analytics): fix first_attempt filter value parsing for Payments (#6667) --- crates/analytics/src/query.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/analytics/src/query.rs b/crates/analytics/src/query.rs index e80f762c41b..caa112ec175 100644 --- a/crates/analytics/src/query.rs +++ b/crates/analytics/src/query.rs @@ -459,7 +459,8 @@ impl ToSql for common_utils::id_type::CustomerId { impl ToSql for bool { fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { - Ok(self.to_string().to_owned()) + let flag = *self; + Ok(i8::from(flag).to_string()) } } From b1cdff0950f32b38e3ff0eeac2b726ba0f671051 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:16:31 +0530 Subject: [PATCH 36/51] fix(opensearch): handle empty free-text query search in global search (#6685) --- crates/analytics/src/opensearch.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/analytics/src/opensearch.rs b/crates/analytics/src/opensearch.rs index 84a2b9db3d4..e8726840a2e 100644 --- a/crates/analytics/src/opensearch.rs +++ b/crates/analytics/src/opensearch.rs @@ -510,14 +510,15 @@ impl OpenSearchQueryBuilder { case_sensitive_filters: Vec<&(String, Vec)>, ) -> Vec { let mut filter_array = Vec::new(); - - filter_array.push(json!({ - "multi_match": { - "type": "phrase", - "query": self.query, - "lenient": true - } - })); + if !self.query.is_empty() { + filter_array.push(json!({ + "multi_match": { + "type": "phrase", + "query": self.query, + "lenient": true + } + })); + } let case_sensitive_json_filters = case_sensitive_filters .into_iter() From 05726262e6a3f6fcb18c0dbe41c18e4d6e84608b Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:26:57 +0530 Subject: [PATCH 37/51] refactor(router): [ZSL] remove partially capture status (#6689) --- .../src/connectors/zsl/transformers.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/zsl/transformers.rs b/crates/hyperswitch_connectors/src/connectors/zsl/transformers.rs index 81019325113..b2ba05b7cde 100644 --- a/crates/hyperswitch_connectors/src/connectors/zsl/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/zsl/transformers.rs @@ -414,20 +414,10 @@ impl TryFrom() .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let txn_amount = item - .response - .txn_amt - .parse::() - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let status = if txn_amount > paid_amount { - enums::AttemptStatus::PartialCharged - } else { - enums::AttemptStatus::Charged - }; if item.response.status == "0" { Ok(Self { - status, + status: enums::AttemptStatus::Charged, amount_captured: Some(paid_amount), response: Ok(PaymentsResponseData::TransactionResponse { resource_id: ResponseId::ConnectorTransactionId(item.response.txn_id.clone()), From 6a2070172b8d845e6db36b7789defddf8ea4e1e9 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:28:14 +0530 Subject: [PATCH 38/51] fix(router): populate card network in the network transaction id based MIT flow (#6690) --- config/config.example.toml | 3 +++ config/deployments/production.toml | 2 ++ crates/router/src/connector/cybersource/transformers.rs | 2 +- crates/router/src/core/payments/helpers.rs | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index ba14ed881cf..76a38192909 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -787,6 +787,9 @@ check_token_status_url= "" # base url to check token status from token servic [network_tokenization_supported_connectors] connector_list = "cybersource" # Supported connectors for network tokenization +[network_transaction_id_supported_connectors] +connector_list = "stripe,adyen,cybersource" # Supported connectors for network transaction id + [grpc_client.dynamic_routing_client] # Dynamic Routing Client Configuration host = "localhost" # Client Host port = 7000 # Client Port diff --git a/config/deployments/production.toml b/config/deployments/production.toml index a859d08ac4a..c266b94bba6 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -176,6 +176,8 @@ bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # M card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card card.debit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card +[network_transaction_id_supported_connectors] +connector_list = "stripe,adyen,cybersource" [payouts] payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 22fa8965b7a..2ea36e3e35e 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -620,7 +620,7 @@ impl .as_ref() .map(|card_network| match card_network.to_lowercase().as_str() { "amex" => "internet", - "discover" => "dipb", + "discover" => "internet", "mastercard" => "spa", "visa" => "internet", _ => "internet", diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 8435f09e8f3..219a4d90519 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4674,7 +4674,7 @@ pub async fn get_additional_payment_data( api_models::payments::AdditionalPaymentData::Card(Box::new( api_models::payments::AdditionalCardInfo { card_issuer: card_info.card_issuer, - card_network, + card_network: card_info.card_network, bank_code: card_info.bank_code, card_type: card_info.card_type, card_issuing_country: card_info.card_issuing_country, From 9998c557c9c88496ffbee883e7fc4b76614cff50 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:32:21 +0530 Subject: [PATCH 39/51] feat(connector): [Adyen] Fetch email from customer email for payment request (#6676) --- .../src/connector/adyen/transformers.rs | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index b6dea0eac21..12cef3aa9f7 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -728,7 +728,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for JCSVoucherData { Ok(Self { first_name: item.get_billing_first_name()?, last_name: item.get_optional_billing_last_name(), - shopper_email: item.get_billing_email()?, + shopper_email: item.get_billing_email().or(item.request.get_email())?, telephone_number: item.get_billing_phone_number()?, }) } @@ -2552,11 +2552,18 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for DokuBankData { Ok(Self { first_name: item.get_billing_first_name()?, last_name: item.get_optional_billing_last_name(), - shopper_email: item.get_billing_email()?, + shopper_email: item.get_billing_email().or(item.request.get_email())?, }) } } +fn get_optional_shopper_email(item: &types::PaymentsAuthorizeRouterData) -> Option { + match item.get_billing_email() { + Ok(email) => Some(email), + Err(_) => item.request.get_optional_email(), + } +} + impl<'a> TryFrom<&domain::payments::CardRedirectData> for AdyenPaymentMethod<'a> { type Error = Error; fn try_from( @@ -2593,6 +2600,7 @@ impl<'a> let amount = get_amount_data(item); let auth_type = AdyenAuthType::try_from(&item.router_data.connector_auth_type)?; let shopper_interaction = AdyenShopperInteraction::from(item.router_data); + let shopper_email = get_optional_shopper_email(item.router_data); let (recurring_processing_model, store_payment_method, shopper_reference) = get_recurring_processing_model(item.router_data)?; let browser_info = None; @@ -2694,7 +2702,7 @@ impl<'a> mpi_data: None, telephone_number: None, shopper_name: None, - shopper_email: None, + shopper_email, shopper_locale: None, social_security_number: None, billing_address: None, @@ -2742,7 +2750,7 @@ impl<'a> let return_url = item.router_data.request.get_return_url()?; let card_holder_name = item.router_data.get_optional_billing_full_name(); let payment_method = AdyenPaymentMethod::try_from((card_data, card_holder_name))?; - let shopper_email = item.router_data.get_optional_billing_email(); + let shopper_email = get_optional_shopper_email(item.router_data); let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); Ok(AdyenPaymentRequest { @@ -2801,6 +2809,7 @@ impl<'a> let return_url = item.router_data.request.get_return_url()?; let payment_method = AdyenPaymentMethod::try_from((bank_debit_data, item.router_data))?; let country_code = get_country_code(item.router_data.get_optional_billing()); + let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -2814,7 +2823,7 @@ impl<'a> mpi_data: None, shopper_name: None, shopper_locale: None, - shopper_email: item.router_data.get_optional_billing_email(), + shopper_email, social_security_number: None, telephone_number: None, billing_address: None, @@ -2860,6 +2869,7 @@ impl<'a> let billing_address = get_address_info(item.router_data.get_optional_billing()).and_then(Result::ok); let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); + let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, @@ -2873,7 +2883,7 @@ impl<'a> additional_data, shopper_name, shopper_locale: None, - shopper_email: item.router_data.get_optional_billing_email(), + shopper_email, social_security_number, mpi_data: None, telephone_number: None, @@ -2913,6 +2923,7 @@ impl<'a> let shopper_interaction = AdyenShopperInteraction::from(item.router_data); let payment_method = AdyenPaymentMethod::try_from((bank_transfer_data, item.router_data))?; let return_url = item.router_data.request.get_return_url()?; + let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -2926,7 +2937,7 @@ impl<'a> mpi_data: None, shopper_name: None, shopper_locale: None, - shopper_email: item.router_data.get_optional_billing_email(), + shopper_email, social_security_number: None, telephone_number: None, billing_address: None, @@ -2965,6 +2976,7 @@ impl<'a> let shopper_interaction = AdyenShopperInteraction::from(item.router_data); let return_url = item.router_data.request.get_router_return_url()?; let payment_method = AdyenPaymentMethod::try_from(gift_card_data)?; + let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -2978,7 +2990,7 @@ impl<'a> mpi_data: None, shopper_name: None, shopper_locale: None, - shopper_email: item.router_data.get_optional_billing_email(), + shopper_email, telephone_number: None, billing_address: None, delivery_address: None, @@ -3028,6 +3040,7 @@ impl<'a> let line_items = Some(get_line_items(item)); let billing_address = get_address_info(item.router_data.get_optional_billing()).and_then(Result::ok); + let shopper_email = get_optional_shopper_email(item.router_data); Ok(AdyenPaymentRequest { amount, @@ -3042,7 +3055,7 @@ impl<'a> mpi_data: None, telephone_number: None, shopper_name: None, - shopper_email: item.router_data.get_optional_billing_email(), + shopper_email, shopper_locale, social_security_number: None, billing_address, @@ -3094,11 +3107,13 @@ fn get_shopper_email( .as_ref() .ok_or(errors::ConnectorError::MissingPaymentMethodType)?; match payment_method_type { - storage_enums::PaymentMethodType::Paypal => Ok(Some(item.get_billing_email()?)), - _ => Ok(item.get_optional_billing_email()), + storage_enums::PaymentMethodType::Paypal => { + Ok(Some(item.get_billing_email().or(item.request.get_email())?)) + } + _ => Ok(get_optional_shopper_email(item)), } } else { - Ok(item.get_optional_billing_email()) + Ok(get_optional_shopper_email(item)) } } @@ -3205,7 +3220,7 @@ impl<'a> get_recurring_processing_model(item.router_data)?; let return_url = item.router_data.request.get_return_url()?; let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); - let shopper_email = item.router_data.get_optional_billing_email(); + let shopper_email = get_optional_shopper_email(item.router_data); let billing_address = get_address_info(item.router_data.get_optional_billing()).and_then(Result::ok); let delivery_address = @@ -3273,7 +3288,7 @@ impl<'a> let shopper_interaction = AdyenShopperInteraction::from(item.router_data); let return_url = item.router_data.request.get_return_url()?; let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); - let shopper_email = item.router_data.get_optional_billing_email(); + let shopper_email = get_optional_shopper_email(item.router_data); let telephone_number = item .router_data .get_billing_phone() From ae7d16e23699c8ed95a7e2eab7539cfe20f847d0 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Fri, 29 Nov 2024 15:32:56 +0530 Subject: [PATCH 40/51] refactor(currency_conversion): release redis lock if api call fails (#6671) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/utils/currency.rs | 45 ++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs index 2173478ab67..9ab2780da73 100644 --- a/crates/router/src/utils/currency.rs +++ b/crates/router/src/utils/currency.rs @@ -7,6 +7,7 @@ use error_stack::ResultExt; use masking::PeekInterface; use once_cell::sync::Lazy; use redis_interface::DelReply; +use router_env::{instrument, tracing}; use rust_decimal::Decimal; use strum::IntoEnumIterator; use tokio::{sync::RwLock, time::sleep}; @@ -150,11 +151,13 @@ impl TryFrom for ExchangeRates { let mut conversion_usable: HashMap = HashMap::new(); for (curr, conversion) in value.conversion { let enum_curr = enums::Currency::from_str(curr.as_str()) - .change_context(ForexCacheError::ConversionError)?; + .change_context(ForexCacheError::ConversionError) + .attach_printable("Unable to Convert currency received")?; conversion_usable.insert(enum_curr, CurrencyFactors::from(conversion)); } let base_curr = enums::Currency::from_str(value.base_currency.as_str()) - .change_context(ForexCacheError::ConversionError)?; + .change_context(ForexCacheError::ConversionError) + .attach_printable("Unable to convert base currency")?; Ok(Self { base_currency: base_curr, conversion: conversion_usable, @@ -170,6 +173,8 @@ impl From for CurrencyFactors { } } } + +#[instrument(skip_all)] pub async fn get_forex_rates( state: &SessionState, call_delay: i64, @@ -235,6 +240,7 @@ async fn successive_fetch_and_save_forex( Ok(rates) => Ok(successive_save_data_to_redis_local(state, rates).await?), Err(error) => stale_redis_data.ok_or({ logger::error!(?error); + release_redis_lock(state).await?; ForexCacheError::ApiUnresponsive.into() }), } @@ -254,9 +260,9 @@ async fn successive_save_data_to_redis_local( ) -> CustomResult { Ok(save_forex_to_redis(state, &forex) .await - .async_and_then(|_rates| async { release_redis_lock(state).await }) + .async_and_then(|_rates| release_redis_lock(state)) .await - .async_and_then(|_val| async { Ok(save_forex_to_local(forex.clone()).await) }) + .async_and_then(|_val| save_forex_to_local(forex.clone())) .await .map_or_else( |error| { @@ -336,11 +342,15 @@ async fn fetch_forex_rates( false, ) .await - .change_context(ForexCacheError::ApiUnresponsive)?; + .change_context(ForexCacheError::ApiUnresponsive) + .attach_printable("Primary forex fetch api unresponsive")?; let forex_response = response .json::() .await - .change_context(ForexCacheError::ParsingError)?; + .change_context(ForexCacheError::ParsingError) + .attach_printable( + "Unable to parse response received from primary api into ForexResponse", + )?; logger::info!("{:?}", forex_response); @@ -392,11 +402,16 @@ pub async fn fallback_fetch_forex_rates( false, ) .await - .change_context(ForexCacheError::ApiUnresponsive)?; + .change_context(ForexCacheError::ApiUnresponsive) + .attach_printable("Fallback forex fetch api unresponsive")?; + let fallback_forex_response = response .json::() .await - .change_context(ForexCacheError::ParsingError)?; + .change_context(ForexCacheError::ParsingError) + .attach_printable( + "Unable to parse response received from falback api into ForexResponse", + )?; logger::info!("{:?}", fallback_forex_response); let mut conversions: HashMap = HashMap::new(); @@ -453,6 +468,7 @@ async fn release_redis_lock( .delete_key(REDIX_FOREX_CACHE_KEY) .await .change_context(ForexCacheError::RedisLockReleaseFailed) + .attach_printable("Unable to release redis lock") } async fn acquire_redis_lock(state: &SessionState) -> CustomResult { @@ -475,6 +491,7 @@ async fn acquire_redis_lock(state: &SessionState) -> CustomResult Date: Fri, 29 Nov 2024 15:34:11 +0530 Subject: [PATCH 41/51] feat(connector): [REDSYS] add Connector Template Code (#6659) --- config/config.example.toml | 1 + 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 + .../hyperswitch_connectors/src/connectors.rs | 6 +- .../src/connectors/redsys.rs | 563 ++++++++++++++++++ .../src/connectors/redsys/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 | 9 +- .../connector_integration_v2_impls.rs | 3 + crates/router/src/core/payments/flows.rs | 3 + crates/router/src/types/api.rs | 1 + crates/router/src/types/transformers.rs | 1 + crates/router/tests/connectors/main.rs | 1 + crates/router/tests/connectors/redsys.rs | 421 +++++++++++++ .../router/tests/connectors/sample_auth.toml | 3 + crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 1 + scripts/add_connector.sh | 2 +- 25 files changed, 1302 insertions(+), 7 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/redsys.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/redsys/transformers.rs create mode 100644 crates/router/tests/connectors/redsys.rs diff --git a/config/config.example.toml b/config/config.example.toml index 76a38192909..03ebedf0d35 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -252,6 +252,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" razorpay.base_url = "https://sandbox.juspay.in/" +redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago" riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index a4e1b1e9b13..fbf80ced5f1 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -93,6 +93,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" razorpay.base_url = "https://sandbox.juspay.in/" +redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" riskified.base_url = "https://sandbox.riskified.com/api" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index c266b94bba6..befd70795d7 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -97,6 +97,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" razorpay.base_url = "https://api.juspay.in" +redsys.base_url = "https://sis.redsys.es:25443/sis/realizarPago" riskified.base_url = "https://wh.riskified.com/api/" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 070a32ef87b..2defc5729cf 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -97,6 +97,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" razorpay.base_url = "https://sandbox.juspay.in/" +redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago" riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" diff --git a/config/development.toml b/config/development.toml index 2388607a489..d8251cfce7b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -155,6 +155,7 @@ cards = [ "plaid", "powertranz", "prophetpay", + "redsys", "shift4", "square", "stax", @@ -268,6 +269,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" razorpay.base_url = "https://sandbox.juspay.in/" +redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago" riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index d72141d9c37..976a2fa2a42 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -183,6 +183,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" razorpay.base_url = "https://sandbox.juspay.in/" +redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago" riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" @@ -276,6 +277,7 @@ cards = [ "plaid", "powertranz", "prophetpay", + "redsys", "shift4", "square", "stax", diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 3d027c026d7..4931b8dbd92 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -113,6 +113,7 @@ pub enum Connector { Prophetpay, Rapyd, Razorpay, + // Redsys, Shift4, Square, Stax, @@ -251,6 +252,7 @@ impl Connector { | Self::Powertranz | Self::Prophetpay | Self::Rapyd + // | Self::Redsys | Self::Shift4 | Self::Square | Self::Stax diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index c3bbf6e078f..421a51205de 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -108,6 +108,7 @@ pub enum RoutableConnectors { Prophetpay, Rapyd, Razorpay, + // Redsys, Riskified, Shift4, Signifyd, diff --git a/crates/hyperswitch_connectors/src/connectors.rs b/crates/hyperswitch_connectors/src/connectors.rs index d1cdb85e57f..fdd87a25e8a 100644 --- a/crates/hyperswitch_connectors/src/connectors.rs +++ b/crates/hyperswitch_connectors/src/connectors.rs @@ -28,6 +28,7 @@ pub mod payeezy; pub mod payu; pub mod powertranz; pub mod razorpay; +pub mod redsys; pub mod shift4; pub mod square; pub mod stax; @@ -49,6 +50,7 @@ pub use self::{ helcim::Helcim, inespay::Inespay, jpmorgan::Jpmorgan, mollie::Mollie, multisafepay::Multisafepay, 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, xendit::Xendit, zen::Zen, zsl::Zsl, + redsys::Redsys, shift4::Shift4, square::Square, stax::Stax, taxjar::Taxjar, thunes::Thunes, + tsys::Tsys, volt::Volt, worldline::Worldline, worldpay::Worldpay, xendit::Xendit, zen::Zen, + zsl::Zsl, }; diff --git a/crates/hyperswitch_connectors/src/connectors/redsys.rs b/crates/hyperswitch_connectors/src/connectors/redsys.rs new file mode 100644 index 00000000000..760ce148b40 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/redsys.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 redsys; + +use crate::{constants::headers, types::ResponseRouterData, utils}; + +#[derive(Clone)] +pub struct Redsys { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Redsys { + pub fn new() -> &'static Self { + &Self { + amount_converter: &StringMinorUnitForConnector, + } + } +} + +impl api::Payment for Redsys {} +impl api::PaymentSession for Redsys {} +impl api::ConnectorAccessToken for Redsys {} +impl api::MandateSetup for Redsys {} +impl api::PaymentAuthorize for Redsys {} +impl api::PaymentSync for Redsys {} +impl api::PaymentCapture for Redsys {} +impl api::PaymentVoid for Redsys {} +impl api::Refund for Redsys {} +impl api::RefundExecute for Redsys {} +impl api::RefundSync for Redsys {} +impl api::PaymentToken for Redsys {} + +impl ConnectorIntegration + for Redsys +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Redsys +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 Redsys { + fn id(&self) -> &'static str { + "redsys" + } + + 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.redsys.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = redsys::RedsysAuthType::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: redsys::RedsysErrorResponse = res + .response + .parse_struct("RedsysErrorResponse") + .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 Redsys { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration for Redsys { + //TODO: implement sessions flow +} + +impl ConnectorIntegration for Redsys {} + +impl ConnectorIntegration for Redsys {} + +impl ConnectorIntegration for Redsys { + 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 = redsys::RedsysRouterData::from((amount, req)); + let connector_req = redsys::RedsysPaymentsRequest::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: redsys::RedsysPaymentsResponse = res + .response + .parse_struct("Redsys 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 Redsys { + 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: redsys::RedsysPaymentsResponse = res + .response + .parse_struct("redsys 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 Redsys { + 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: redsys::RedsysPaymentsResponse = res + .response + .parse_struct("Redsys 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 Redsys {} + +impl ConnectorIntegration for Redsys { + 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 = redsys::RedsysRouterData::from((refund_amount, req)); + let connector_req = redsys::RedsysRefundRequest::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: redsys::RefundResponse = + res.response + .parse_struct("redsys 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 Redsys { + 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: redsys::RefundResponse = res + .response + .parse_struct("redsys 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 Redsys { + 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/redsys/transformers.rs b/crates/hyperswitch_connectors/src/connectors/redsys/transformers.rs new file mode 100644 index 00000000000..78329b5719d --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/redsys/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 RedsysRouterData { + 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 RedsysRouterData { + 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 RedsysPaymentsRequest { + amount: StringMinorUnit, + card: RedsysCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct RedsysCard { + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&RedsysRouterData<&PaymentsAuthorizeRouterData>> for RedsysPaymentsRequest { + type Error = error_stack::Report; + fn try_from( + item: &RedsysRouterData<&PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(req_card) => { + let card = RedsysCard { + 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 RedsysAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&ConnectorAuthType> for RedsysAuthType { + 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 RedsysPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for common_enums::AttemptStatus { + fn from(item: RedsysPaymentStatus) -> Self { + match item { + RedsysPaymentStatus::Succeeded => Self::Charged, + RedsysPaymentStatus::Failed => Self::Failure, + RedsysPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct RedsysPaymentsResponse { + status: RedsysPaymentStatus, + 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 RedsysRefundRequest { + pub amount: StringMinorUnit, +} + +impl TryFrom<&RedsysRouterData<&RefundsRouterData>> for RedsysRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &RedsysRouterData<&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 RedsysErrorResponse { + 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 50b28be2b0b..25306458e8f 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -119,6 +119,7 @@ default_imp_for_authorize_session_token!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Taxjar, @@ -177,6 +178,7 @@ default_imp_for_calculate_tax!( connectors::Payu, connectors::Powertranz, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -223,6 +225,7 @@ default_imp_for_session_update!( connectors::Inespay, connectors::Jpmorgan, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -283,6 +286,7 @@ default_imp_for_post_session_tokens!( connectors::Inespay, connectors::Jpmorgan, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Taxjar, @@ -348,6 +352,7 @@ default_imp_for_complete_authorize!( connectors::Payeezy, connectors::Payu, connectors::Razorpay, + connectors::Redsys, connectors::Stax, connectors::Square, connectors::Taxjar, @@ -406,6 +411,7 @@ default_imp_for_incremental_authorization!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -466,6 +472,7 @@ default_imp_for_create_customer!( connectors::Payu, connectors::Powertranz, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Square, connectors::Taxjar, @@ -522,6 +529,7 @@ default_imp_for_connector_redirect_response!( connectors::Payu, connectors::Powertranz, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -578,6 +586,7 @@ default_imp_for_pre_processing_steps!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Stax, connectors::Square, connectors::Taxjar, @@ -637,6 +646,7 @@ default_imp_for_post_processing_steps!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -697,6 +707,7 @@ default_imp_for_approve!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -757,6 +768,7 @@ default_imp_for_reject!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -817,6 +829,7 @@ default_imp_for_webhook_source_verification!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -878,6 +891,7 @@ default_imp_for_accept_dispute!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -938,6 +952,7 @@ default_imp_for_submit_evidence!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -998,6 +1013,7 @@ default_imp_for_defend_dispute!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1067,6 +1083,7 @@ default_imp_for_file_upload!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1120,6 +1137,7 @@ default_imp_for_payouts!( connectors::Payu, connectors::Powertranz, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Square, connectors::Stax, @@ -1181,6 +1199,7 @@ default_imp_for_payouts_create!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1243,6 +1262,7 @@ default_imp_for_payouts_retrieve!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1305,6 +1325,7 @@ default_imp_for_payouts_eligibility!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1367,6 +1388,7 @@ default_imp_for_payouts_fulfill!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1429,6 +1451,7 @@ default_imp_for_payouts_cancel!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1491,6 +1514,7 @@ default_imp_for_payouts_quote!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1553,6 +1577,7 @@ default_imp_for_payouts_recipient!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1615,6 +1640,7 @@ default_imp_for_payouts_recipient_account!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1677,6 +1703,7 @@ default_imp_for_frm_sale!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1739,6 +1766,7 @@ default_imp_for_frm_checkout!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1801,6 +1829,7 @@ default_imp_for_frm_transaction!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1863,6 +1892,7 @@ default_imp_for_frm_fulfillment!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1925,6 +1955,7 @@ default_imp_for_frm_record_return!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1984,6 +2015,7 @@ default_imp_for_revoking_mandates!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index 7b19ca68365..6a30a180fe7 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -235,6 +235,7 @@ default_imp_for_new_connector_integration_payment!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -296,6 +297,7 @@ default_imp_for_new_connector_integration_refund!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -352,6 +354,7 @@ default_imp_for_new_connector_integration_connector_access_token!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -414,6 +417,7 @@ default_imp_for_new_connector_integration_accept_dispute!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -475,6 +479,7 @@ default_imp_for_new_connector_integration_submit_evidence!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -536,6 +541,7 @@ default_imp_for_new_connector_integration_defend_dispute!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -607,6 +613,7 @@ default_imp_for_new_connector_integration_file_upload!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -670,6 +677,7 @@ default_imp_for_new_connector_integration_payouts_create!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -733,6 +741,7 @@ default_imp_for_new_connector_integration_payouts_eligibility!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -796,6 +805,7 @@ default_imp_for_new_connector_integration_payouts_fulfill!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -859,6 +869,7 @@ default_imp_for_new_connector_integration_payouts_cancel!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -922,6 +933,7 @@ default_imp_for_new_connector_integration_payouts_quote!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -985,6 +997,7 @@ default_imp_for_new_connector_integration_payouts_recipient!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1048,6 +1061,7 @@ default_imp_for_new_connector_integration_payouts_sync!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1111,6 +1125,7 @@ default_imp_for_new_connector_integration_payouts_recipient_account!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1172,6 +1187,7 @@ default_imp_for_new_connector_integration_webhook_source_verification!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1235,6 +1251,7 @@ default_imp_for_new_connector_integration_frm_sale!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1298,6 +1315,7 @@ default_imp_for_new_connector_integration_frm_checkout!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1361,6 +1379,7 @@ default_imp_for_new_connector_integration_frm_transaction!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1424,6 +1443,7 @@ default_imp_for_new_connector_integration_frm_fulfillment!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1487,6 +1507,7 @@ default_imp_for_new_connector_integration_frm_record_return!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, @@ -1547,6 +1568,7 @@ default_imp_for_new_connector_integration_revoking_mandates!( connectors::Mollie, connectors::Multisafepay, connectors::Razorpay, + connectors::Redsys, connectors::Shift4, connectors::Stax, connectors::Square, diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index 539b87c4808..d6e195bbec4 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -76,6 +76,7 @@ pub struct Connectors { pub prophetpay: ConnectorParams, pub rapyd: ConnectorParams, pub razorpay: ConnectorParamsWithKeys, + pub redsys: ConnectorParams, pub riskified: ConnectorParams, pub shift4: ConnectorParams, pub signifyd: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index b6668323ba9..5874f4ba463 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -54,10 +54,11 @@ pub use hyperswitch_connectors::connectors::{ inespay::Inespay, jpmorgan, jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, multisafepay::Multisafepay, nexinets, nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, nomupay, nomupay::Nomupay, novalnet, novalnet::Novalnet, payeezy, payeezy::Payeezy, payu, - payu::Payu, powertranz, 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, xendit, xendit::Xendit, zen, zen::Zen, zsl, zsl::Zsl, + payu::Payu, powertranz, powertranz::Powertranz, razorpay, razorpay::Razorpay, redsys, + redsys::Redsys, 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, xendit, xendit::Xendit, zen, zen::Zen, zsl, + zsl::Zsl, }; #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index 44e8c25d67b..8afd019d080 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -1170,6 +1170,7 @@ default_imp_for_new_connector_integration_payouts!( connector::Prophetpay, connector::Rapyd, connector::Razorpay, + connector::Redsys, connector::Riskified, connector::Signifyd, connector::Square, @@ -1818,6 +1819,7 @@ default_imp_for_new_connector_integration_frm!( connector::Prophetpay, connector::Rapyd, connector::Razorpay, + connector::Redsys, connector::Riskified, connector::Signifyd, connector::Square, @@ -2314,6 +2316,7 @@ default_imp_for_new_connector_integration_connector_authentication!( connector::Prophetpay, connector::Rapyd, connector::Razorpay, + connector::Redsys, connector::Riskified, connector::Signifyd, connector::Square, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 9ba260f554f..df319724bc4 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -511,6 +511,7 @@ default_imp_for_connector_request_id!( connector::Prophetpay, connector::Rapyd, connector::Razorpay, + connector::Redsys, connector::Riskified, connector::Shift4, connector::Signifyd, @@ -1799,6 +1800,7 @@ default_imp_for_fraud_check!( connector::Prophetpay, connector::Rapyd, connector::Razorpay, + connector::Redsys, connector::Shift4, connector::Square, connector::Stax, @@ -2462,6 +2464,7 @@ default_imp_for_connector_authentication!( connector::Prophetpay, connector::Rapyd, connector::Razorpay, + connector::Redsys, connector::Riskified, connector::Shift4, connector::Signifyd, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index d550c1978b2..f5fa2d9a37e 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -486,6 +486,7 @@ impl ConnectorData { enums::Connector::Rapyd => { Ok(ConnectorEnum::Old(Box::new(connector::Rapyd::new()))) } + // enums::Connector::Redsys => Ok(ConnectorEnum::Old(Box::new(connector::Redsys))), enums::Connector::Shift4 => { Ok(ConnectorEnum::Old(Box::new(connector::Shift4::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 78138757493..2ff243ec4b9 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -280,6 +280,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Prophetpay => Self::Prophetpay, api_enums::Connector::Rapyd => Self::Rapyd, api_enums::Connector::Razorpay => Self::Razorpay, + // api_enums::Connector::Redsys => Self::Redsys, api_enums::Connector::Shift4 => Self::Shift4, api_enums::Connector::Signifyd => { Err(common_utils::errors::ValidationError::InvalidValue { diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index ef3ae2d14db..dcedb171675 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -74,6 +74,7 @@ mod powertranz; mod prophetpay; mod rapyd; mod razorpay; +mod redsys; mod shift4; mod square; mod stax; diff --git a/crates/router/tests/connectors/redsys.rs b/crates/router/tests/connectors/redsys.rs new file mode 100644 index 00000000000..532bbb6f550 --- /dev/null +++ b/crates/router/tests/connectors/redsys.rs @@ -0,0 +1,421 @@ +use hyperswitch_domain_models::payment_method_data::{Card, PaymentMethodData}; +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct RedsysTest; +impl ConnectorActions for RedsysTest {} +impl utils::Connector for RedsysTest { + fn get_data(&self) -> api::ConnectorData { + use router::connector::Redsys; + utils::construct_connector_data_old( + Box::new(Redsys::new()), + types::Connector::Plaid, + api::GetToken::Connector, + None, + ) + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .redsys + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "redsys".to_string() + } +} + +static CONNECTOR: RedsysTest = RedsysTest {}; + +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: PaymentMethodData::Card(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: PaymentMethodData::Card(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: PaymentMethodData::Card(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/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 120ce5e9d26..d099c16254b 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -268,6 +268,9 @@ api_key="API Key" [nexixpay] api_key="API Key" +[redsys] +api_key="API Key" + [wellsfargopayout] api_key = "Consumer Key" key1 = "Gateway Entity Id" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 4bb348d6679..3fab02e64d1 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -77,6 +77,7 @@ pub struct ConnectorAuthentication { pub prophetpay: Option, pub rapyd: Option, pub razorpay: Option, + pub redsys: Option, pub shift4: Option, pub square: Option, pub stax: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index a3ac1159ddb..81bcf01fddc 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -149,6 +149,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" razorpay.base_url = "https://sandbox.juspay.in/" +redsys.base_url = "https://sis-t.redsys.es:25443/sis/realizarPago" riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index e5a65128319..fd555a41613 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 inespay 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") + 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 inespay 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 redsys 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 b1d1073389f58c480a53a27be24aa91554520ff1 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:38:22 +0530 Subject: [PATCH 42/51] feat(payments): [Payment links] add showCardFormByDefault config for payment links (#6663) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 14 +++++++++++++- api-reference/openapi_spec.json | 14 +++++++++++++- crates/api_models/src/admin.rs | 5 +++++ crates/api_models/src/payments.rs | 2 ++ crates/diesel_models/src/business_profile.rs | 1 + crates/diesel_models/src/payment_intent.rs | 2 ++ crates/hyperswitch_domain_models/src/lib.rs | 3 +++ crates/router/src/consts.rs | 3 +++ crates/router/src/core/payment_link.rs | 11 +++++++++-- .../payment_link_initiator.js | 1 + .../secure_payment_link_initiator.js | 1 + crates/router/src/core/payments/transformers.rs | 2 ++ crates/router/src/types/transformers.rs | 2 ++ 13 files changed, 57 insertions(+), 4 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 0edeb537dc1..15103188df5 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -11993,7 +11993,8 @@ "sdk_layout", "display_sdk_only", "enabled_saved_payment_method", - "hide_card_nickname_field" + "hide_card_nickname_field", + "show_card_form_by_default" ], "properties": { "theme": { @@ -12024,6 +12025,10 @@ "type": "boolean", "description": "Hide card nickname field option for payment link" }, + "show_card_form_by_default": { + "type": "boolean", + "description": "Show card form by default for payment link" + }, "allowed_domains": { "type": "array", "items": { @@ -12095,6 +12100,13 @@ "example": true, "nullable": true }, + "show_card_form_by_default": { + "type": "boolean", + "description": "Show card form by default for payment link", + "default": true, + "example": true, + "nullable": true + }, "transaction_details": { "type": "array", "items": { diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index f624b4f68dd..ecce327d7ff 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -15170,7 +15170,8 @@ "sdk_layout", "display_sdk_only", "enabled_saved_payment_method", - "hide_card_nickname_field" + "hide_card_nickname_field", + "show_card_form_by_default" ], "properties": { "theme": { @@ -15201,6 +15202,10 @@ "type": "boolean", "description": "Hide card nickname field option for payment link" }, + "show_card_form_by_default": { + "type": "boolean", + "description": "Show card form by default for payment link" + }, "allowed_domains": { "type": "array", "items": { @@ -15272,6 +15277,13 @@ "example": true, "nullable": true }, + "show_card_form_by_default": { + "type": "boolean", + "description": "Show card form by default for payment link", + "default": true, + "example": true, + "nullable": true + }, "transaction_details": { "type": "array", "items": { diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 8ba50649236..7e1465c9d92 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2673,6 +2673,9 @@ pub struct PaymentLinkConfigRequest { /// Hide card nickname field option for payment link #[schema(default = false, example = true)] pub hide_card_nickname_field: Option, + /// Show card form by default for payment link + #[schema(default = true, example = true)] + pub show_card_form_by_default: Option, /// Dynamic details related to merchant to be rendered in payment link pub transaction_details: Option>, } @@ -2718,6 +2721,8 @@ pub struct PaymentLinkConfig { pub enabled_saved_payment_method: bool, /// Hide card nickname field option for payment link pub hide_card_nickname_field: bool, + /// Show card form by default for payment link + pub show_card_form_by_default: bool, /// A list of allowed domains (glob patterns) where this link can be embedded / opened from pub allowed_domains: Option>, /// Dynamic details related to merchant to be rendered in payment link diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 45b9bac53ac..fb60937e9b1 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6670,6 +6670,7 @@ pub struct PaymentLinkDetails { pub sdk_layout: String, pub display_sdk_only: bool, pub hide_card_nickname_field: bool, + pub show_card_form_by_default: bool, pub locale: Option, pub transaction_details: Option>, } @@ -6678,6 +6679,7 @@ pub struct PaymentLinkDetails { pub struct SecurePaymentLinkDetails { pub enabled_saved_payment_method: bool, pub hide_card_nickname_field: bool, + pub show_card_form_by_default: bool, #[serde(flatten)] pub payment_link_details: PaymentLinkDetails, } diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index f4c7b86850e..1f8209397ae 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -543,6 +543,7 @@ pub struct PaymentLinkConfigRequest { pub display_sdk_only: Option, pub enabled_saved_payment_method: Option, pub hide_card_nickname_field: Option, + pub show_card_form_by_default: Option, } common_utils::impl_to_sql_from_sql_json!(BusinessPaymentLinkConfig); diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 7826e2dadd2..9f0bf17230d 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -156,6 +156,8 @@ pub struct PaymentLinkConfigRequestForPayments { pub enabled_saved_payment_method: Option, /// Hide card nickname field option for payment link pub hide_card_nickname_field: Option, + /// Show card form by default for payment link + pub show_card_form_by_default: Option, /// Dynamic details related to merchant to be rendered in payment link pub transaction_details: Option>, } diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 64c6c97a0fd..d94e921c120 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -192,6 +192,7 @@ impl ApiModelToDieselModelConvertor display_sdk_only: item.display_sdk_only, enabled_saved_payment_method: item.enabled_saved_payment_method, hide_card_nickname_field: item.hide_card_nickname_field, + show_card_form_by_default: item.show_card_form_by_default, transaction_details: item.transaction_details.map(|transaction_details| { transaction_details .into_iter() @@ -213,6 +214,7 @@ impl ApiModelToDieselModelConvertor display_sdk_only, enabled_saved_payment_method, hide_card_nickname_field, + show_card_form_by_default, transaction_details, } = self; api_models::admin::PaymentLinkConfigRequest { @@ -223,6 +225,7 @@ impl ApiModelToDieselModelConvertor display_sdk_only, enabled_saved_payment_method, hide_card_nickname_field, + show_card_form_by_default, transaction_details: transaction_details.map(|transaction_details| { transaction_details .into_iter() diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 51385593e9d..9b02c67ce6a 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -153,6 +153,9 @@ pub const DEFAULT_ALLOWED_DOMAINS: Option> = None; /// Default hide card nickname field pub const DEFAULT_HIDE_CARD_NICKNAME_FIELD: bool = false; +/// Show card form by default for payment links +pub const DEFAULT_SHOW_CARD_FORM: bool = true; + /// Default bool for Display sdk only pub const DEFAULT_DISPLAY_SDK_ONLY: bool = false; diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 91829e56f2c..3555eb1193e 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -24,7 +24,7 @@ use crate::{ consts::{ self, DEFAULT_ALLOWED_DOMAINS, DEFAULT_BACKGROUND_COLOR, DEFAULT_DISPLAY_SDK_ONLY, DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, DEFAULT_HIDE_CARD_NICKNAME_FIELD, - DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, + DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SHOW_CARD_FORM, }, errors::RouterResponse, get_payment_link_config_value, get_payment_link_config_value_based_on_priority, @@ -126,6 +126,7 @@ pub async fn form_payment_link_data( display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY, enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, hide_card_nickname_field: DEFAULT_HIDE_CARD_NICKNAME_FIELD, + show_card_form_by_default: DEFAULT_SHOW_CARD_FORM, allowed_domains: DEFAULT_ALLOWED_DOMAINS, transaction_details: None, } @@ -267,6 +268,7 @@ pub async fn form_payment_link_data( sdk_layout: payment_link_config.sdk_layout.clone(), display_sdk_only: payment_link_config.display_sdk_only, hide_card_nickname_field: payment_link_config.hide_card_nickname_field, + show_card_form_by_default: payment_link_config.show_card_form_by_default, locale, transaction_details: payment_link_config.transaction_details.clone(), }; @@ -325,6 +327,7 @@ pub async fn initiate_secure_payment_link_flow( let secure_payment_link_details = api_models::payments::SecurePaymentLinkDetails { enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method, hide_card_nickname_field: payment_link_config.hide_card_nickname_field, + show_card_form_by_default: payment_link_config.show_card_form_by_default, payment_link_details: *link_details.to_owned(), }; let js_script = format!( @@ -618,6 +621,7 @@ pub fn get_payment_link_config_based_on_priority( display_sdk_only, enabled_saved_payment_method, hide_card_nickname_field, + show_card_form_by_default, ) = get_payment_link_config_value!( payment_create_link_config, business_theme_configs, @@ -630,7 +634,8 @@ pub fn get_payment_link_config_based_on_priority( enabled_saved_payment_method, DEFAULT_ENABLE_SAVED_PAYMENT_METHOD ), - (hide_card_nickname_field, DEFAULT_HIDE_CARD_NICKNAME_FIELD) + (hide_card_nickname_field, DEFAULT_HIDE_CARD_NICKNAME_FIELD), + (show_card_form_by_default, DEFAULT_SHOW_CARD_FORM) ); let payment_link_config = PaymentLinkConfig { theme, @@ -640,6 +645,7 @@ pub fn get_payment_link_config_based_on_priority( display_sdk_only, enabled_saved_payment_method, hide_card_nickname_field, + show_card_form_by_default, allowed_domains, transaction_details: payment_create_link_config .and_then(|payment_link_config| payment_link_config.theme_config.transaction_details), @@ -743,6 +749,7 @@ pub async fn get_payment_link_status( display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY, enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, hide_card_nickname_field: DEFAULT_HIDE_CARD_NICKNAME_FIELD, + show_card_form_by_default: DEFAULT_SHOW_CARD_FORM, allowed_domains: DEFAULT_ALLOWED_DOMAINS, transaction_details: None, } diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js index 1264915592d..b79e2284a56 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js @@ -56,6 +56,7 @@ function initializeSDK() { height: 55, }, }, + showCardFormByDefault: paymentDetails.show_card_form_by_default, hideCardNicknameField: false, }; // @ts-ignore diff --git a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js index 5080970ce3c..4bddc6904be 100644 --- a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js +++ b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js @@ -81,6 +81,7 @@ if (!isFramed) { }, }, hideCardNicknameField: hideCardNicknameField, + showCardFormByDefault: paymentDetails.show_card_form_by_default, }; // @ts-ignore unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 4baeade05ab..9e0557d5b3e 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -3699,6 +3699,7 @@ impl ForeignFrom display_sdk_only: config.display_sdk_only, enabled_saved_payment_method: config.enabled_saved_payment_method, hide_card_nickname_field: config.hide_card_nickname_field, + show_card_form_by_default: config.show_card_form_by_default, transaction_details: config.transaction_details.map(|transaction_details| { transaction_details .iter() @@ -3752,6 +3753,7 @@ impl ForeignFrom display_sdk_only: config.display_sdk_only, enabled_saved_payment_method: config.enabled_saved_payment_method, hide_card_nickname_field: config.hide_card_nickname_field, + show_card_form_by_default: config.show_card_form_by_default, transaction_details: config.transaction_details.map(|transaction_details| { transaction_details .iter() diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2ff243ec4b9..4ae02668957 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1945,6 +1945,7 @@ impl ForeignFrom display_sdk_only: item.display_sdk_only, enabled_saved_payment_method: item.enabled_saved_payment_method, hide_card_nickname_field: item.hide_card_nickname_field, + show_card_form_by_default: item.show_card_form_by_default, } } } @@ -1961,6 +1962,7 @@ impl ForeignFrom display_sdk_only: item.display_sdk_only, enabled_saved_payment_method: item.enabled_saved_payment_method, hide_card_nickname_field: item.hide_card_nickname_field, + show_card_form_by_default: item.show_card_form_by_default, transaction_details: None, } } From 880ad1e883fb42f73c2805287e64bc2c2dcbb9f3 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:39:24 +0530 Subject: [PATCH 43/51] fix(users): Mark user as verified if user logins from SSO (#6694) --- crates/router/src/core/user.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 2087d01dbb4..b181cf797e3 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -2345,13 +2345,24 @@ pub async fn sso_sign( .await?; // TODO: Use config to handle not found error - let user_from_db = state + let user_from_db: domain::UserFromStorage = state .global_store .find_user_by_email(&email.into_inner()) .await .map(Into::into) .to_not_found_response(UserErrors::UserNotFound)?; + if !user_from_db.is_verified() { + state + .global_store + .update_user_by_user_id( + user_from_db.get_user_id(), + storage_user::UserUpdate::VerifyUser, + ) + .await + .change_context(UserErrors::InternalServerError)?; + } + let next_flow = if let Some(user_from_single_purpose_token) = user_from_single_purpose_token { let current_flow = domain::CurrentFlow::new(user_from_single_purpose_token, domain::SPTFlow::SSO.into())?; From 9212f77684b04115332d9be5c3d20bdc56b02160 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:39:55 +0530 Subject: [PATCH 44/51] feat(users): add tenant id reads in user roles (#6661) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/diesel_models/src/query/user_role.rs | 208 +++++++++--------- crates/router/src/analytics.rs | 2 + crates/router/src/core/user.rs | 56 +++++ crates/router/src/core/user_role.rs | 69 ++++++ crates/router/src/db/kafka_store.rs | 6 + crates/router/src/db/user_role.rs | 82 +++++-- crates/router/src/services/authentication.rs | 3 + .../src/types/domain/user/decision_manager.rs | 33 ++- crates/router/src/utils/user_role.rs | 7 + 9 files changed, 340 insertions(+), 126 deletions(-) diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index ed018cc2381..bb07f671824 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -1,7 +1,11 @@ use async_bb8_diesel::AsyncRunQueryDsl; use common_utils::id_type; use diesel::{ - associations::HasTable, debug_query, pg::Pg, result::Error as DieselError, + associations::HasTable, + debug_query, + pg::Pg, + result::Error as DieselError, + sql_types::{Bool, Nullable}, BoolExpressionMethods, ExpressionMethods, QueryDsl, }; use error_stack::{report, ResultExt}; @@ -22,89 +26,70 @@ impl UserRoleNew { } impl UserRole { - pub async fn find_by_user_id( - conn: &PgPooledConn, - user_id: String, - version: UserRoleVersion, - ) -> StorageResult { - generics::generic_find_one::<::Table, _, _>( - conn, - dsl::user_id.eq(user_id).and(dsl::version.eq(version)), - ) - .await - } - - pub async fn find_by_user_id_merchant_id( - conn: &PgPooledConn, - user_id: String, - merchant_id: id_type::MerchantId, - version: UserRoleVersion, - ) -> StorageResult { - generics::generic_find_one::<::Table, _, _>( - conn, - dsl::user_id - .eq(user_id) - .and(dsl::merchant_id.eq(merchant_id)) - .and(dsl::version.eq(version)), - ) - .await - } - - pub async fn list_by_user_id( - conn: &PgPooledConn, - user_id: String, - version: UserRoleVersion, - ) -> StorageResult> { - generics::generic_filter::<::Table, _, _, _>( - conn, - dsl::user_id.eq(user_id).and(dsl::version.eq(version)), - None, - None, - Some(dsl::created_at.asc()), - ) - .await - } - - pub async fn list_by_merchant_id( - conn: &PgPooledConn, - merchant_id: id_type::MerchantId, - version: UserRoleVersion, - ) -> StorageResult> { - generics::generic_filter::<::Table, _, _, _>( - conn, - dsl::merchant_id - .eq(merchant_id) - .and(dsl::version.eq(version)), - None, - None, - Some(dsl::created_at.asc()), + fn check_user_in_lineage( + tenant_id: id_type::TenantId, + org_id: Option, + merchant_id: Option, + profile_id: Option, + ) -> Box< + dyn diesel::BoxableExpression<::Table, Pg, SqlType = Nullable> + + 'static, + > { + // Checking in user roles, for a user in token hierarchy, only one of the relations will be true: + // either tenant level, org level, merchant level, or profile level + // Tenant-level: (tenant_id = ? && org_id = null && merchant_id = null && profile_id = null) + // Org-level: (org_id = ? && merchant_id = null && profile_id = null) + // Merchant-level: (org_id = ? && merchant_id = ? && profile_id = null) + // Profile-level: (org_id = ? && merchant_id = ? && profile_id = ?) + Box::new( + // Tenant-level condition + dsl::tenant_id + .eq(tenant_id.clone()) + .and(dsl::org_id.is_null()) + .and(dsl::merchant_id.is_null()) + .and(dsl::profile_id.is_null()) + .or( + // Org-level condition + dsl::tenant_id + .eq(tenant_id.clone()) + .and(dsl::org_id.eq(org_id.clone())) + .and(dsl::merchant_id.is_null()) + .and(dsl::profile_id.is_null()), + ) + .or( + // Merchant-level condition + dsl::tenant_id + .eq(tenant_id.clone()) + .and(dsl::org_id.eq(org_id.clone())) + .and(dsl::merchant_id.eq(merchant_id.clone())) + .and(dsl::profile_id.is_null()), + ) + .or( + // Profile-level condition + dsl::tenant_id + .eq(tenant_id) + .and(dsl::org_id.eq(org_id)) + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::profile_id.eq(profile_id)), + ), ) - .await } - pub async fn find_by_user_id_org_id_merchant_id_profile_id( + pub async fn find_by_user_id_tenant_id_org_id_merchant_id_profile_id( conn: &PgPooledConn, user_id: String, + tenant_id: id_type::TenantId, org_id: id_type::OrganizationId, merchant_id: id_type::MerchantId, profile_id: id_type::ProfileId, version: UserRoleVersion, ) -> StorageResult { - // Checking in user roles, for a user in token hierarchy, only one of the relation will be true, either org level, merchant level or profile level - // (org_id = ? && merchant_id = null && profile_id = null) || (org_id = ? && merchant_id = ? && profile_id = null) || (org_id = ? && merchant_id = ? && profile_id = ?) - let check_lineage = dsl::org_id - .eq(org_id.clone()) - .and(dsl::merchant_id.is_null().and(dsl::profile_id.is_null())) - .or(dsl::org_id.eq(org_id.clone()).and( - dsl::merchant_id - .eq(merchant_id.clone()) - .and(dsl::profile_id.is_null()), - )) - .or(dsl::org_id.eq(org_id).and( - dsl::merchant_id - .eq(merchant_id) - .and(dsl::profile_id.eq(profile_id)), - )); + let check_lineage = Self::check_user_in_lineage( + tenant_id, + Some(org_id), + Some(merchant_id), + Some(profile_id), + ); let predicate = dsl::user_id .eq(user_id) @@ -114,30 +99,46 @@ impl UserRole { generics::generic_find_one::<::Table, _, _>(conn, predicate).await } - pub async fn update_by_user_id_org_id_merchant_id_profile_id( + #[allow(clippy::too_many_arguments)] + pub async fn update_by_user_id_tenant_id_org_id_merchant_id_profile_id( conn: &PgPooledConn, user_id: String, + tenant_id: id_type::TenantId, org_id: id_type::OrganizationId, merchant_id: Option, profile_id: Option, update: UserRoleUpdate, version: UserRoleVersion, ) -> StorageResult { - // Checking in user roles, for a user in token hierarchy, only one of the relation will be true, either org level, merchant level or profile level - // (org_id = ? && merchant_id = null && profile_id = null) || (org_id = ? && merchant_id = ? && profile_id = null) || (org_id = ? && merchant_id = ? && profile_id = ?) - let check_lineage = dsl::org_id - .eq(org_id.clone()) - .and(dsl::merchant_id.is_null().and(dsl::profile_id.is_null())) - .or(dsl::org_id.eq(org_id.clone()).and( - dsl::merchant_id - .eq(merchant_id.clone()) + let check_lineage = dsl::tenant_id + .eq(tenant_id.clone()) + .and(dsl::org_id.is_null()) + .and(dsl::merchant_id.is_null()) + .and(dsl::profile_id.is_null()) + .or( + // Org-level condition + dsl::tenant_id + .eq(tenant_id.clone()) + .and(dsl::org_id.eq(org_id.clone())) + .and(dsl::merchant_id.is_null()) + .and(dsl::profile_id.is_null()), + ) + .or( + // Merchant-level condition + dsl::tenant_id + .eq(tenant_id.clone()) + .and(dsl::org_id.eq(org_id.clone())) + .and(dsl::merchant_id.eq(merchant_id.clone())) .and(dsl::profile_id.is_null()), - )) - .or(dsl::org_id.eq(org_id).and( - dsl::merchant_id - .eq(merchant_id) + ) + .or( + // Profile-level condition + dsl::tenant_id + .eq(tenant_id) + .and(dsl::org_id.eq(org_id)) + .and(dsl::merchant_id.eq(merchant_id)) .and(dsl::profile_id.eq(profile_id)), - )); + ); let predicate = dsl::user_id .eq(user_id) @@ -153,29 +154,21 @@ impl UserRole { .await } - pub async fn delete_by_user_id_org_id_merchant_id_profile_id( + pub async fn delete_by_user_id_tenant_id_org_id_merchant_id_profile_id( conn: &PgPooledConn, user_id: String, + tenant_id: id_type::TenantId, org_id: id_type::OrganizationId, merchant_id: id_type::MerchantId, profile_id: id_type::ProfileId, version: UserRoleVersion, ) -> StorageResult { - // Checking in user roles, for a user in token hierarchy, only one of the relation will be true, either org level, merchant level or profile level - // (org_id = ? && merchant_id = null && profile_id = null) || (org_id = ? && merchant_id = ? && profile_id = null) || (org_id = ? && merchant_id = ? && profile_id = ?) - let check_lineage = dsl::org_id - .eq(org_id.clone()) - .and(dsl::merchant_id.is_null().and(dsl::profile_id.is_null())) - .or(dsl::org_id.eq(org_id.clone()).and( - dsl::merchant_id - .eq(merchant_id.clone()) - .and(dsl::profile_id.is_null()), - )) - .or(dsl::org_id.eq(org_id).and( - dsl::merchant_id - .eq(merchant_id) - .and(dsl::profile_id.eq(profile_id)), - )); + let check_lineage = Self::check_user_in_lineage( + tenant_id, + Some(org_id), + Some(merchant_id), + Some(profile_id), + ); let predicate = dsl::user_id .eq(user_id) @@ -190,6 +183,7 @@ impl UserRole { pub async fn generic_user_roles_list_for_user( conn: &PgPooledConn, user_id: String, + tenant_id: id_type::TenantId, org_id: Option, merchant_id: Option, profile_id: Option, @@ -199,7 +193,7 @@ impl UserRole { limit: Option, ) -> StorageResult> { let mut query = ::table() - .filter(dsl::user_id.eq(user_id)) + .filter(dsl::user_id.eq(user_id).and(dsl::tenant_id.eq(tenant_id))) .into_boxed(); if let Some(org_id) = org_id { @@ -248,9 +242,11 @@ impl UserRole { } } + #[allow(clippy::too_many_arguments)] pub async fn generic_user_roles_list_for_org_and_extra( conn: &PgPooledConn, user_id: Option, + tenant_id: id_type::TenantId, org_id: id_type::OrganizationId, merchant_id: Option, profile_id: Option, @@ -258,7 +254,7 @@ impl UserRole { limit: Option, ) -> StorageResult> { let mut query = ::table() - .filter(dsl::org_id.eq(org_id)) + .filter(dsl::org_id.eq(org_id).and(dsl::tenant_id.eq(tenant_id))) .into_boxed(); if let Some(user_id) = user_id { diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 96f41f75ee0..d957c3071ff 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1864,6 +1864,7 @@ pub mod routes { .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &auth.user_id, + tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), org_id: Some(&auth.org_id), merchant_id: None, profile_id: None, @@ -1987,6 +1988,7 @@ pub mod routes { .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &auth.user_id, + tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), org_id: Some(&auth.org_id), merchant_id: None, profile_id: None, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index b181cf797e3..22623c4ca66 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -614,6 +614,10 @@ async fn handle_existing_user_invitation( .global_store .find_user_role_by_user_id_and_lineage( invitee_user_from_db.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -630,6 +634,10 @@ async fn handle_existing_user_invitation( .global_store .find_user_role_by_user_id_and_lineage( invitee_user_from_db.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -660,6 +668,10 @@ async fn handle_existing_user_invitation( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: invitee_user_from_db.get_user_id(), + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id, merchant_id, profile_id, @@ -962,6 +974,10 @@ pub async fn resend_invite( .global_store .find_user_role_by_user_id_and_lineage( user.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -985,6 +1001,10 @@ pub async fn resend_invite( .global_store .find_user_role_by_user_id_and_lineage( user.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -1064,6 +1084,10 @@ pub async fn accept_invite_from_email_token_only_flow( utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite( &state, &user_token.user_id, + user_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), entity.entity_id.clone(), entity.entity_type, ) @@ -1074,6 +1098,10 @@ pub async fn accept_invite_from_email_token_only_flow( let (update_v1_result, update_v2_result) = utils::user_role::update_v1_and_v2_user_roles_in_db( &state, user_from_db.get_user_id(), + user_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &org_id, merchant_id.as_ref(), profile_id.as_ref(), @@ -1260,6 +1288,10 @@ pub async fn list_user_roles_details( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: required_user.get_user_id(), + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: Some(&user_from_token.org_id), merchant_id: (requestor_role_info.get_entity_type() <= EntityType::Merchant) .then_some(&user_from_token.merchant_id), @@ -2446,6 +2478,10 @@ pub async fn list_orgs_for_user( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_token.user_id.as_str(), + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: None, merchant_id: None, profile_id: None, @@ -2511,6 +2547,10 @@ pub async fn list_merchants_for_user_in_org( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_token.user_id.as_str(), + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: Some(&user_from_token.org_id), merchant_id: None, profile_id: None, @@ -2590,6 +2630,10 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_token.user_id.as_str(), + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: Some(&user_from_token.org_id), merchant_id: Some(&user_from_token.merchant_id), profile_id: None, @@ -2666,6 +2710,10 @@ pub async fn switch_org_for_user( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &user_from_token.user_id, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: Some(&request.org_id), merchant_id: None, profile_id: None, @@ -2839,6 +2887,10 @@ pub async fn switch_merchant_for_user_in_org( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &user_from_token.user_id, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: Some(&user_from_token.org_id), merchant_id: Some(&request.merchant_id), profile_id: None, @@ -2955,6 +3007,10 @@ pub async fn switch_profile_for_user_in_org_and_merchant( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload{ user_id:&user_from_token.user_id, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: Some(&user_from_token.org_id), merchant_id: Some(&user_from_token.merchant_id), profile_id:Some(&request.profile_id), diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 6641e553fd8..eaa655a07f3 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -159,6 +159,10 @@ pub async fn update_user_role( .global_store .find_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -213,6 +217,10 @@ pub async fn update_user_role( .global_store .update_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, Some(&user_from_token.merchant_id), Some(&user_from_token.profile_id), @@ -232,6 +240,10 @@ pub async fn update_user_role( .global_store .find_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -286,6 +298,10 @@ pub async fn update_user_role( .global_store .update_user_role_by_user_id_and_lineage( user_to_be_updated.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, Some(&user_from_token.merchant_id), Some(&user_from_token.profile_id), @@ -320,6 +336,10 @@ pub async fn accept_invitations_v2( utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite( &state, &user_from_token.user_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), entity.entity_id, entity.entity_type, ) @@ -335,6 +355,10 @@ pub async fn accept_invitations_v2( utils::user_role::update_v1_and_v2_user_roles_in_db( &state, user_from_token.user_id.as_str(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id, merchant_id.as_ref(), profile_id.as_ref(), @@ -372,6 +396,10 @@ pub async fn accept_invitations_pre_auth( utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite( &state, &user_token.user_id, + user_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), entity.entity_id, entity.entity_type, ) @@ -387,6 +415,10 @@ pub async fn accept_invitations_pre_auth( utils::user_role::update_v1_and_v2_user_roles_in_db( &state, user_token.user_id.as_str(), + user_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id, merchant_id.as_ref(), profile_id.as_ref(), @@ -473,6 +505,10 @@ pub async fn delete_user_role( .global_store .find_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -520,6 +556,10 @@ pub async fn delete_user_role( .global_store .delete_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -535,6 +575,10 @@ pub async fn delete_user_role( .global_store .find_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -582,6 +626,10 @@ pub async fn delete_user_role( .global_store .delete_user_role_by_user_id_and_lineage( user_from_db.get_user_id(), + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, &user_from_token.merchant_id, &user_from_token.profile_id, @@ -602,6 +650,11 @@ pub async fn delete_user_role( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user_from_db.get_user_id(), + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + org_id: None, merchant_id: None, profile_id: None, @@ -650,6 +703,10 @@ pub async fn list_users_in_lineage( &state, ListUserRolesByOrgIdPayload { user_id: None, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: &user_from_token.org_id, merchant_id: None, profile_id: None, @@ -665,6 +722,10 @@ pub async fn list_users_in_lineage( &state, ListUserRolesByOrgIdPayload { user_id: None, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: &user_from_token.org_id, merchant_id: Some(&user_from_token.merchant_id), profile_id: None, @@ -680,6 +741,10 @@ pub async fn list_users_in_lineage( &state, ListUserRolesByOrgIdPayload { user_id: None, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: &user_from_token.org_id, merchant_id: Some(&user_from_token.merchant_id), profile_id: Some(&user_from_token.profile_id), @@ -779,6 +844,10 @@ pub async fn list_invitations_for_user( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: &user_from_token.user_id, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), org_id: None, merchant_id: None, profile_id: None, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index e37ff3aa167..436755ea720 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3048,6 +3048,7 @@ impl UserRoleInterface for KafkaStore { async fn find_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, @@ -3056,6 +3057,7 @@ impl UserRoleInterface for KafkaStore { self.diesel_store .find_user_role_by_user_id_and_lineage( user_id, + tenant_id, org_id, merchant_id, profile_id, @@ -3067,6 +3069,7 @@ impl UserRoleInterface for KafkaStore { async fn update_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, profile_id: Option<&id_type::ProfileId>, @@ -3076,6 +3079,7 @@ impl UserRoleInterface for KafkaStore { self.diesel_store .update_user_role_by_user_id_and_lineage( user_id, + tenant_id, org_id, merchant_id, profile_id, @@ -3088,6 +3092,7 @@ impl UserRoleInterface for KafkaStore { async fn delete_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, @@ -3096,6 +3101,7 @@ impl UserRoleInterface for KafkaStore { self.diesel_store .delete_user_role_by_user_id_and_lineage( user_id, + tenant_id, org_id, merchant_id, profile_id, diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index e4e564dc9a4..0da51898326 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -15,6 +15,7 @@ use crate::{ pub struct ListUserRolesByOrgIdPayload<'a> { pub user_id: Option<&'a String>, + pub tenant_id: &'a id_type::TenantId, pub org_id: &'a id_type::OrganizationId, pub merchant_id: Option<&'a id_type::MerchantId>, pub profile_id: Option<&'a id_type::ProfileId>, @@ -24,6 +25,7 @@ pub struct ListUserRolesByOrgIdPayload<'a> { pub struct ListUserRolesByUserIdPayload<'a> { pub user_id: &'a str, + pub tenant_id: &'a id_type::TenantId, pub org_id: Option<&'a id_type::OrganizationId>, pub merchant_id: Option<&'a id_type::MerchantId>, pub profile_id: Option<&'a id_type::ProfileId>, @@ -43,15 +45,18 @@ pub trait UserRoleInterface { async fn find_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult; + #[allow(clippy::too_many_arguments)] async fn update_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, profile_id: Option<&id_type::ProfileId>, @@ -62,6 +67,7 @@ pub trait UserRoleInterface { async fn delete_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, @@ -98,15 +104,17 @@ impl UserRoleInterface for Store { async fn find_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; - storage::UserRole::find_by_user_id_org_id_merchant_id_profile_id( + storage::UserRole::find_by_user_id_tenant_id_org_id_merchant_id_profile_id( &conn, user_id.to_owned(), + tenant_id.to_owned(), org_id.to_owned(), merchant_id.to_owned(), profile_id.to_owned(), @@ -120,6 +128,7 @@ impl UserRoleInterface for Store { async fn update_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, profile_id: Option<&id_type::ProfileId>, @@ -127,9 +136,10 @@ impl UserRoleInterface for Store { version: enums::UserRoleVersion, ) -> CustomResult { let conn = connection::pg_connection_write(self).await?; - storage::UserRole::update_by_user_id_org_id_merchant_id_profile_id( + storage::UserRole::update_by_user_id_tenant_id_org_id_merchant_id_profile_id( &conn, user_id.to_owned(), + tenant_id.to_owned(), org_id.to_owned(), merchant_id.cloned(), profile_id.cloned(), @@ -144,15 +154,17 @@ impl UserRoleInterface for Store { async fn delete_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, version: enums::UserRoleVersion, ) -> CustomResult { let conn = connection::pg_connection_write(self).await?; - storage::UserRole::delete_by_user_id_org_id_merchant_id_profile_id( + storage::UserRole::delete_by_user_id_tenant_id_org_id_merchant_id_profile_id( &conn, user_id.to_owned(), + tenant_id.to_owned(), org_id.to_owned(), merchant_id.to_owned(), profile_id.to_owned(), @@ -170,6 +182,7 @@ impl UserRoleInterface for Store { storage::UserRole::generic_user_roles_list_for_user( &conn, payload.user_id.to_owned(), + payload.tenant_id.to_owned(), payload.org_id.cloned(), payload.merchant_id.cloned(), payload.profile_id.cloned(), @@ -190,6 +203,7 @@ impl UserRoleInterface for Store { storage::UserRole::generic_user_roles_list_for_org_and_extra( &conn, payload.user_id.cloned(), + payload.tenant_id.to_owned(), payload.org_id.to_owned(), payload.merchant_id.cloned(), payload.profile_id.cloned(), @@ -243,6 +257,7 @@ impl UserRoleInterface for MockDb { async fn find_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, @@ -251,21 +266,32 @@ impl UserRoleInterface for MockDb { let user_roles = self.user_roles.lock().await; for user_role in user_roles.iter() { - let org_level_check = user_role.org_id.as_ref() == Some(org_id) + let tenant_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.is_none() && user_role.merchant_id.is_none() && user_role.profile_id.is_none(); - let merchant_level_check = user_role.org_id.as_ref() == Some(org_id) + let org_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.as_ref() == Some(org_id) + && user_role.merchant_id.is_none() + && user_role.profile_id.is_none(); + + let merchant_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.as_ref() == Some(org_id) && user_role.merchant_id.as_ref() == Some(merchant_id) && user_role.profile_id.is_none(); - let profile_level_check = user_role.org_id.as_ref() == Some(org_id) + let profile_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.as_ref() == Some(org_id) && user_role.merchant_id.as_ref() == Some(merchant_id) && user_role.profile_id.as_ref() == Some(profile_id); // Check if any condition matches and the version matches if user_role.user_id == user_id - && (org_level_check || merchant_level_check || profile_level_check) + && (tenant_level_check + || org_level_check + || merchant_level_check + || profile_level_check) && user_role.version == version { return Ok(user_role.clone()); @@ -282,6 +308,7 @@ impl UserRoleInterface for MockDb { async fn update_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, profile_id: Option<&id_type::ProfileId>, @@ -291,21 +318,32 @@ impl UserRoleInterface for MockDb { let mut user_roles = self.user_roles.lock().await; for user_role in user_roles.iter_mut() { - let org_level_check = user_role.org_id.as_ref() == Some(org_id) + let tenant_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.is_none() + && user_role.merchant_id.is_none() + && user_role.profile_id.is_none(); + + let org_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.as_ref() == Some(org_id) && user_role.merchant_id.is_none() && user_role.profile_id.is_none(); - let merchant_level_check = user_role.org_id.as_ref() == Some(org_id) + let merchant_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.as_ref() == Some(org_id) && user_role.merchant_id.as_ref() == merchant_id && user_role.profile_id.is_none(); - let profile_level_check = user_role.org_id.as_ref() == Some(org_id) + let profile_level_check = user_role.tenant_id == *tenant_id + && user_role.org_id.as_ref() == Some(org_id) && user_role.merchant_id.as_ref() == merchant_id && user_role.profile_id.as_ref() == profile_id; - // Check if the user role matches the conditions and the version matches + // Check if any condition matches and the version matches if user_role.user_id == user_id - && (org_level_check || merchant_level_check || profile_level_check) + && (tenant_level_check + || org_level_check + || merchant_level_check + || profile_level_check) && user_role.version == version { match &update { @@ -336,6 +374,7 @@ impl UserRoleInterface for MockDb { async fn delete_user_role_by_user_id_and_lineage( &self, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: &id_type::MerchantId, profile_id: &id_type::ProfileId, @@ -345,21 +384,32 @@ impl UserRoleInterface for MockDb { // Find the position of the user role to delete let index = user_roles.iter().position(|role| { - let org_level_check = role.org_id.as_ref() == Some(org_id) + let tenant_level_check = role.tenant_id == *tenant_id + && role.org_id.is_none() + && role.merchant_id.is_none() + && role.profile_id.is_none(); + + let org_level_check = role.tenant_id == *tenant_id + && role.org_id.as_ref() == Some(org_id) && role.merchant_id.is_none() && role.profile_id.is_none(); - let merchant_level_check = role.org_id.as_ref() == Some(org_id) + let merchant_level_check = role.tenant_id == *tenant_id + && role.org_id.as_ref() == Some(org_id) && role.merchant_id.as_ref() == Some(merchant_id) && role.profile_id.is_none(); - let profile_level_check = role.org_id.as_ref() == Some(org_id) + let profile_level_check = role.tenant_id == *tenant_id + && role.org_id.as_ref() == Some(org_id) && role.merchant_id.as_ref() == Some(merchant_id) && role.profile_id.as_ref() == Some(profile_id); // Check if the user role matches the conditions and the version matches role.user_id == user_id - && (org_level_check || merchant_level_check || profile_level_check) + && (tenant_level_check + || org_level_check + || merchant_level_check + || profile_level_check) && role.version == version }); diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index c05e4514aaa..d50933b708d 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -273,6 +273,7 @@ pub struct UserFromToken { pub struct UserIdFromAuth { pub user_id: String, + pub tenant_id: Option, } #[cfg(feature = "olap")] @@ -858,6 +859,7 @@ where Ok(( UserIdFromAuth { user_id: payload.user_id.clone(), + tenant_id: payload.tenant_id, }, AuthenticationType::SinglePurposeOrLoginJwt { user_id: payload.user_id, @@ -899,6 +901,7 @@ where Ok(( UserIdFromAuth { user_id: payload.user_id.clone(), + tenant_id: payload.tenant_id, }, AuthenticationType::SinglePurposeOrLoginJwt { user_id: payload.user_id, diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs index 634c781da7f..10990da6ccb 100644 --- a/crates/router/src/types/domain/user/decision_manager.rs +++ b/crates/router/src/types/domain/user/decision_manager.rs @@ -1,4 +1,5 @@ use common_enums::TokenPurpose; +use common_utils::id_type; use diesel_models::{enums::UserStatus, user_role::UserRole}; use error_stack::{report, ResultExt}; use masking::Secret; @@ -24,9 +25,10 @@ impl UserFlow { user: &UserFromStorage, path: &[TokenPurpose], state: &SessionState, + user_tenant_id: &id_type::TenantId, ) -> UserResult { match self { - Self::SPTFlow(flow) => flow.is_required(user, path, state).await, + Self::SPTFlow(flow) => flow.is_required(user, path, state, user_tenant_id).await, Self::JWTFlow(flow) => flow.is_required(user, state).await, } } @@ -50,6 +52,7 @@ impl SPTFlow { user: &UserFromStorage, path: &[TokenPurpose], state: &SessionState, + user_tenant_id: &id_type::TenantId, ) -> UserResult { match self { // Auth @@ -68,6 +71,7 @@ impl SPTFlow { .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: user.get_user_id(), + tenant_id: user_tenant_id, org_id: None, merchant_id: None, profile_id: None, @@ -220,6 +224,7 @@ pub struct CurrentFlow { origin: Origin, current_flow_index: usize, path: Vec, + tenant_id: Option, } impl CurrentFlow { @@ -239,6 +244,7 @@ impl CurrentFlow { origin: token.origin, current_flow_index: index, path, + tenant_id: token.tenant_id, }) } @@ -247,12 +253,21 @@ impl CurrentFlow { let remaining_flows = flows.iter().skip(self.current_flow_index + 1); for flow in remaining_flows { - if flow.is_required(&user, &self.path, state).await? { + if flow + .is_required( + &user, + &self.path, + state, + self.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + ) + .await? + { return Ok(NextFlow { origin: self.origin.clone(), next_flow: *flow, user, path: self.path, + tenant_id: self.tenant_id, }); } } @@ -265,6 +280,7 @@ pub struct NextFlow { next_flow: UserFlow, user: UserFromStorage, path: Vec, + tenant_id: Option, } impl NextFlow { @@ -276,12 +292,16 @@ impl NextFlow { let flows = origin.get_flows(); let path = vec![]; for flow in flows { - if flow.is_required(&user, &path, state).await? { + if flow + .is_required(&user, &path, state, &state.tenant.tenant_id) + .await? + { return Ok(Self { origin, next_flow: *flow, user, path, + tenant_id: Some(state.tenant.tenant_id.clone()), }); } } @@ -304,6 +324,7 @@ impl NextFlow { .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id: self.user.get_user_id(), + tenant_id: self.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), org_id: None, merchant_id: None, profile_id: None, @@ -352,12 +373,16 @@ impl NextFlow { .ok_or(UserErrors::InternalServerError)?; let remaining_flows = flows.iter().skip(index + 1); for flow in remaining_flows { - if flow.is_required(&user, &self.path, state).await? { + if flow + .is_required(&user, &self.path, state, &state.tenant.tenant_id) + .await? + { return Ok(Self { origin: self.origin.clone(), next_flow: *flow, user, path: self.path, + tenant_id: Some(state.tenant.tenant_id.clone()), }); } } diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index aaf313a196e..0bd0e81149f 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -133,6 +133,7 @@ pub async fn set_role_permissions_in_cache_if_required( pub async fn update_v1_and_v2_user_roles_in_db( state: &SessionState, user_id: &str, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, profile_id: Option<&id_type::ProfileId>, @@ -145,6 +146,7 @@ pub async fn update_v1_and_v2_user_roles_in_db( .global_store .update_user_role_by_user_id_and_lineage( user_id, + tenant_id, org_id, merchant_id, profile_id, @@ -161,6 +163,7 @@ pub async fn update_v1_and_v2_user_roles_in_db( .global_store .update_user_role_by_user_id_and_lineage( user_id, + tenant_id, org_id, merchant_id, profile_id, @@ -210,6 +213,7 @@ pub async fn get_single_merchant_id( pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( state: &SessionState, user_id: &str, + tenant_id: &id_type::TenantId, entity_id: String, entity_type: EntityType, ) -> UserResult< @@ -231,6 +235,7 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id, + tenant_id, org_id: Some(&org_id), merchant_id: None, profile_id: None, @@ -275,6 +280,7 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id, + tenant_id, org_id: None, merchant_id: Some(&merchant_id), profile_id: None, @@ -320,6 +326,7 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( .global_store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { user_id, + tenant_id: &state.tenant.tenant_id, org_id: None, merchant_id: None, profile_id: Some(&profile_id), From 96393ff3d6b11d4726a6cb2224236414507d9848 Mon Sep 17 00:00:00 2001 From: Anurag Thakur Date: Fri, 29 Nov 2024 15:58:59 +0530 Subject: [PATCH 45/51] fix(openapi): Standardise API naming scheme for V2 (#6510) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../api-reference/api-key/api-key--create.mdx | 2 +- .../api-reference/api-key/api-key--list.mdx | 2 +- .../api-key/api-key--retrieve.mdx | 2 +- .../api-reference/api-key/api-key--revoke.mdx | 2 +- .../api-reference/api-key/api-key--update.mdx | 2 +- .../merchant-connector--list.mdx | 2 +- .../connector-account--create.mdx | 2 +- .../connector-account--delete.mdx | 2 +- .../connector-account--retrieve.mdx | 2 +- .../connector-account--update.mdx | 2 +- .../business-profile--list.mdx | 2 +- .../merchant-account--create.mdx | 2 +- .../merchant-account--retrieve.mdx | 2 +- .../merchant-account--update.mdx | 2 +- .../merchant-account/profile--list.mdx | 3 - .../organization--merchant-account--list.mdx | 2 +- ...er-saved-payment-methods-for-a-payment.mdx | 3 + .../list-payment-methods-for-a-customer.mdx | 3 + .../payment-method--confirm-intent.mdx | 3 + .../payment-method--create-intent.mdx | 3 + .../payment-method--create.mdx | 3 + .../payment-method--delete.mdx | 3 + .../payment-method--retrieve.mdx | 3 + .../payment-method--update.mdx | 3 + .../profile/merchant-connector--list.mdx | 2 +- .../profile--activate-routing-algorithm.mdx | 2 +- .../profile--deactivate-routing-algorithm.mdx | 2 +- ...ile--retrieve-active-routing-algorithm.mdx | 2 +- ...eve-default-fallback-routing-algorithm.mdx | 2 +- ...ate-default-fallback-routing-algorithm.mdx | 2 +- .../api-reference/routing/routing--create.mdx | 2 +- .../routing/routing--retrieve.mdx | 2 +- api-reference-v2/mint.json | 23 +- api-reference-v2/openapi_spec.json | 510 +++++++++++++++++- api-reference/openapi_spec.json | 12 +- crates/api_models/src/organization.rs | 40 +- crates/api_models/src/payment_methods.rs | 2 +- .../hyperswitch_domain_models/src/payments.rs | 4 +- crates/openapi/src/openapi_v2.rs | 15 + crates/openapi/src/routes/api_keys.rs | 10 +- crates/openapi/src/routes/merchant_account.rs | 8 +- .../src/routes/merchant_connector_account.rs | 8 +- crates/openapi/src/routes/organization.rs | 2 +- crates/openapi/src/routes/payment_method.rs | 173 ++++++ crates/openapi/src/routes/profile.rs | 14 +- crates/openapi/src/routes/routing.rs | 4 +- crates/router/src/routes/app.rs | 38 +- crates/router/src/routes/payment_methods.rs | 47 -- crates/router/src/types/api/admin.rs | 4 + cypress-tests-v2/cypress/support/commands.js | 52 +- 50 files changed, 872 insertions(+), 167 deletions(-) delete mode 100644 api-reference-v2/api-reference/merchant-account/profile--list.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/list-customer-saved-payment-methods-for-a-payment.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/list-payment-methods-for-a-customer.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/payment-method--confirm-intent.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/payment-method--create-intent.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/payment-method--create.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/payment-method--delete.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/payment-method--retrieve.mdx create mode 100644 api-reference-v2/api-reference/payment-methods/payment-method--update.mdx diff --git a/api-reference-v2/api-reference/api-key/api-key--create.mdx b/api-reference-v2/api-reference/api-key/api-key--create.mdx index a92a8ea77fd..abc1dcda10f 100644 --- a/api-reference-v2/api-reference/api-key/api-key--create.mdx +++ b/api-reference-v2/api-reference/api-key/api-key--create.mdx @@ -1,3 +1,3 @@ --- -openapi: post /v2/api_keys +openapi: post /v2/api-keys --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/api-key/api-key--list.mdx b/api-reference-v2/api-reference/api-key/api-key--list.mdx index 5975e9bd6ca..fb84b35fbc7 100644 --- a/api-reference-v2/api-reference/api-key/api-key--list.mdx +++ b/api-reference-v2/api-reference/api-key/api-key--list.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/api_keys/list +openapi: get /v2/api-keys/list --- diff --git a/api-reference-v2/api-reference/api-key/api-key--retrieve.mdx b/api-reference-v2/api-reference/api-key/api-key--retrieve.mdx index ee7970122d4..72864363357 100644 --- a/api-reference-v2/api-reference/api-key/api-key--retrieve.mdx +++ b/api-reference-v2/api-reference/api-key/api-key--retrieve.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/api_keys/{id} +openapi: get /v2/api-keys/{id} --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/api-key/api-key--revoke.mdx b/api-reference-v2/api-reference/api-key/api-key--revoke.mdx index 9362743088b..b7ffd42e449 100644 --- a/api-reference-v2/api-reference/api-key/api-key--revoke.mdx +++ b/api-reference-v2/api-reference/api-key/api-key--revoke.mdx @@ -1,3 +1,3 @@ --- -openapi: delete /v2/api_keys/{id} +openapi: delete /v2/api-keys/{id} --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/api-key/api-key--update.mdx b/api-reference-v2/api-reference/api-key/api-key--update.mdx index c682cf1ee9e..2434e4981fc 100644 --- a/api-reference-v2/api-reference/api-key/api-key--update.mdx +++ b/api-reference-v2/api-reference/api-key/api-key--update.mdx @@ -1,3 +1,3 @@ --- -openapi: put /v2/api_keys/{id} +openapi: put /v2/api-keys/{id} --- diff --git a/api-reference-v2/api-reference/business-profile/merchant-connector--list.mdx b/api-reference-v2/api-reference/business-profile/merchant-connector--list.mdx index 6560f45e5fa..93c5a980c27 100644 --- a/api-reference-v2/api-reference/business-profile/merchant-connector--list.mdx +++ b/api-reference-v2/api-reference/business-profile/merchant-connector--list.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/profiles/{profile_id}/connector_accounts +openapi: get /v2/profiles/{profile_id}/connector-accounts --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/connector-account/connector-account--create.mdx b/api-reference-v2/api-reference/connector-account/connector-account--create.mdx index d8cac2bab39..d672d6fa34d 100644 --- a/api-reference-v2/api-reference/connector-account/connector-account--create.mdx +++ b/api-reference-v2/api-reference/connector-account/connector-account--create.mdx @@ -1,3 +1,3 @@ --- -openapi: post /v2/connector_accounts +openapi: post /v2/connector-accounts --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/connector-account/connector-account--delete.mdx b/api-reference-v2/api-reference/connector-account/connector-account--delete.mdx index 5c959648fff..15fdd664412 100644 --- a/api-reference-v2/api-reference/connector-account/connector-account--delete.mdx +++ b/api-reference-v2/api-reference/connector-account/connector-account--delete.mdx @@ -1,3 +1,3 @@ --- -openapi: delete /v2/connector_accounts/{id} +openapi: delete /v2/connector-accounts/{id} --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/connector-account/connector-account--retrieve.mdx b/api-reference-v2/api-reference/connector-account/connector-account--retrieve.mdx index 918de031276..dbd26b9b10b 100644 --- a/api-reference-v2/api-reference/connector-account/connector-account--retrieve.mdx +++ b/api-reference-v2/api-reference/connector-account/connector-account--retrieve.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/connector_accounts/{id} +openapi: get /v2/connector-accounts/{id} --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/connector-account/connector-account--update.mdx b/api-reference-v2/api-reference/connector-account/connector-account--update.mdx index 6ccd052fb9b..fe864d538f8 100644 --- a/api-reference-v2/api-reference/connector-account/connector-account--update.mdx +++ b/api-reference-v2/api-reference/connector-account/connector-account--update.mdx @@ -1,3 +1,3 @@ --- -openapi: put /v2/connector_accounts/{id} +openapi: put /v2/connector-accounts/{id} --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/merchant-account/business-profile--list.mdx b/api-reference-v2/api-reference/merchant-account/business-profile--list.mdx index 97deb0832cc..069bd602ddf 100644 --- a/api-reference-v2/api-reference/merchant-account/business-profile--list.mdx +++ b/api-reference-v2/api-reference/merchant-account/business-profile--list.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/merchant_accounts/{id}/profiles +openapi: get /v2/merchant-accounts/{id}/profiles --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/merchant-account/merchant-account--create.mdx b/api-reference-v2/api-reference/merchant-account/merchant-account--create.mdx index d870b811aae..38aed603f62 100644 --- a/api-reference-v2/api-reference/merchant-account/merchant-account--create.mdx +++ b/api-reference-v2/api-reference/merchant-account/merchant-account--create.mdx @@ -1,3 +1,3 @@ --- -openapi: post /v2/merchant_accounts +openapi: post /v2/merchant-accounts --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/merchant-account/merchant-account--retrieve.mdx b/api-reference-v2/api-reference/merchant-account/merchant-account--retrieve.mdx index d082565234e..3b744fb1406 100644 --- a/api-reference-v2/api-reference/merchant-account/merchant-account--retrieve.mdx +++ b/api-reference-v2/api-reference/merchant-account/merchant-account--retrieve.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/merchant_accounts/{id} +openapi: get /v2/merchant-accounts/{id} --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/merchant-account/merchant-account--update.mdx b/api-reference-v2/api-reference/merchant-account/merchant-account--update.mdx index 51f80ceea30..eb2e92d0652 100644 --- a/api-reference-v2/api-reference/merchant-account/merchant-account--update.mdx +++ b/api-reference-v2/api-reference/merchant-account/merchant-account--update.mdx @@ -1,3 +1,3 @@ --- -openapi: put /v2/merchant_accounts/{id} +openapi: put /v2/merchant-accounts/{id} --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/merchant-account/profile--list.mdx b/api-reference-v2/api-reference/merchant-account/profile--list.mdx deleted file mode 100644 index e14bc0d6ef3..00000000000 --- a/api-reference-v2/api-reference/merchant-account/profile--list.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: get /v2/merchant_accounts/{account_id}/profiles ---- \ No newline at end of file diff --git a/api-reference-v2/api-reference/organization/organization--merchant-account--list.mdx b/api-reference-v2/api-reference/organization/organization--merchant-account--list.mdx index 58d467dc572..9a03e8713d1 100644 --- a/api-reference-v2/api-reference/organization/organization--merchant-account--list.mdx +++ b/api-reference-v2/api-reference/organization/organization--merchant-account--list.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/organization/{id}/merchant_accounts +openapi: get /v2/organization/{id}/merchant-accounts --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/list-customer-saved-payment-methods-for-a-payment.mdx b/api-reference-v2/api-reference/payment-methods/list-customer-saved-payment-methods-for-a-payment.mdx new file mode 100644 index 00000000000..7809830b820 --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/list-customer-saved-payment-methods-for-a-payment.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/payments/{id}/saved-payment-methods +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/list-payment-methods-for-a-customer.mdx b/api-reference-v2/api-reference/payment-methods/list-payment-methods-for-a-customer.mdx new file mode 100644 index 00000000000..ef5a27f9604 --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/list-payment-methods-for-a-customer.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/customers/{id}/saved-payment-methods +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/payment-method--confirm-intent.mdx b/api-reference-v2/api-reference/payment-methods/payment-method--confirm-intent.mdx new file mode 100644 index 00000000000..134374a7b6c --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/payment-method--confirm-intent.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/payment-methods/{id}/confirm-intent +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/payment-method--create-intent.mdx b/api-reference-v2/api-reference/payment-methods/payment-method--create-intent.mdx new file mode 100644 index 00000000000..42cf716f2ab --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/payment-method--create-intent.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/payment-methods/create-intent +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/payment-method--create.mdx b/api-reference-v2/api-reference/payment-methods/payment-method--create.mdx new file mode 100644 index 00000000000..1dce5179a94 --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/payment-method--create.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/payment-methods +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/payment-method--delete.mdx b/api-reference-v2/api-reference/payment-methods/payment-method--delete.mdx new file mode 100644 index 00000000000..210bf843f97 --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/payment-method--delete.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /v2/payment-methods/{id} +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/payment-method--retrieve.mdx b/api-reference-v2/api-reference/payment-methods/payment-method--retrieve.mdx new file mode 100644 index 00000000000..957d9760b3f --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/payment-method--retrieve.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/payment-methods/{id} +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-methods/payment-method--update.mdx b/api-reference-v2/api-reference/payment-methods/payment-method--update.mdx new file mode 100644 index 00000000000..0adee195a6f --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/payment-method--update.mdx @@ -0,0 +1,3 @@ +--- +openapi: patch /v2/payment-methods/{id}/update-saved-payment-method +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/profile/merchant-connector--list.mdx b/api-reference-v2/api-reference/profile/merchant-connector--list.mdx index 81f640436f4..55218be7c0b 100644 --- a/api-reference-v2/api-reference/profile/merchant-connector--list.mdx +++ b/api-reference-v2/api-reference/profile/merchant-connector--list.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/profiles/{id}/connector_accounts +openapi: get /v2/profiles/{id}/connector-accounts --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/profile/profile--activate-routing-algorithm.mdx b/api-reference-v2/api-reference/profile/profile--activate-routing-algorithm.mdx index 7225f422e5a..ea9ee7596a0 100644 --- a/api-reference-v2/api-reference/profile/profile--activate-routing-algorithm.mdx +++ b/api-reference-v2/api-reference/profile/profile--activate-routing-algorithm.mdx @@ -1,3 +1,3 @@ --- -openapi: patch /v2/profiles/{id}/activate_routing_algorithm +openapi: patch /v2/profiles/{id}/activate-routing-algorithm --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/profile/profile--deactivate-routing-algorithm.mdx b/api-reference-v2/api-reference/profile/profile--deactivate-routing-algorithm.mdx index 87aac8b9379..4d6b2d620c6 100644 --- a/api-reference-v2/api-reference/profile/profile--deactivate-routing-algorithm.mdx +++ b/api-reference-v2/api-reference/profile/profile--deactivate-routing-algorithm.mdx @@ -1,3 +1,3 @@ --- -openapi: patch /v2/profiles/{id}/deactivate_routing_algorithm +openapi: patch /v2/profiles/{id}/deactivate-routing-algorithm --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/profile/profile--retrieve-active-routing-algorithm.mdx b/api-reference-v2/api-reference/profile/profile--retrieve-active-routing-algorithm.mdx index 86d2d35d57c..143837676c2 100644 --- a/api-reference-v2/api-reference/profile/profile--retrieve-active-routing-algorithm.mdx +++ b/api-reference-v2/api-reference/profile/profile--retrieve-active-routing-algorithm.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/profiles/{id}/routing_algorithm +openapi: get /v2/profiles/{id}/routing-algorithm --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/profile/profile--retrieve-default-fallback-routing-algorithm.mdx b/api-reference-v2/api-reference/profile/profile--retrieve-default-fallback-routing-algorithm.mdx index 1bc383c278f..ebaad7c53ae 100644 --- a/api-reference-v2/api-reference/profile/profile--retrieve-default-fallback-routing-algorithm.mdx +++ b/api-reference-v2/api-reference/profile/profile--retrieve-default-fallback-routing-algorithm.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/profiles/{id}/fallback_routing +openapi: get /v2/profiles/{id}/fallback-routing --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/profile/profile--update-default-fallback-routing-algorithm.mdx b/api-reference-v2/api-reference/profile/profile--update-default-fallback-routing-algorithm.mdx index 76f4d4fa77f..b5df6a57ef8 100644 --- a/api-reference-v2/api-reference/profile/profile--update-default-fallback-routing-algorithm.mdx +++ b/api-reference-v2/api-reference/profile/profile--update-default-fallback-routing-algorithm.mdx @@ -1,3 +1,3 @@ --- -openapi: patch /v2/profiles/{id}/fallback_routing +openapi: patch /v2/profiles/{id}/fallback-routing --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/routing/routing--create.mdx b/api-reference-v2/api-reference/routing/routing--create.mdx index 65ef15008f2..438abd8e231 100644 --- a/api-reference-v2/api-reference/routing/routing--create.mdx +++ b/api-reference-v2/api-reference/routing/routing--create.mdx @@ -1,3 +1,3 @@ --- -openapi: post /v2/routing_algorithm +openapi: post /v2/routing-algorithm --- \ No newline at end of file diff --git a/api-reference-v2/api-reference/routing/routing--retrieve.mdx b/api-reference-v2/api-reference/routing/routing--retrieve.mdx index 776ff69e004..10db0200e18 100644 --- a/api-reference-v2/api-reference/routing/routing--retrieve.mdx +++ b/api-reference-v2/api-reference/routing/routing--retrieve.mdx @@ -1,3 +1,3 @@ --- -openapi: get /v2/routing_algorithm/{id} +openapi: get /v2/routing-algorithm/{id} --- \ No newline at end of file diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index c0723a63f3a..aed89492443 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -23,7 +23,9 @@ "navigation": [ { "group": "Get Started", - "pages": ["introduction"] + "pages": [ + "introduction" + ] }, { "group": "Essentials", @@ -43,6 +45,19 @@ "api-reference/payments/payments--get" ] }, + { + "group": "Payment Methods", + "pages": [ + "api-reference/payment-methods/payment-method--create", + "api-reference/payment-methods/payment-method--retrieve", + "api-reference/payment-methods/payment-method--update", + "api-reference/payment-methods/payment-method--delete", + "api-reference/payment-methods/payment-method--create-intent", + "api-reference/payment-methods/payment-method--confirm-intent", + "api-reference/payment-methods/list-customer-saved-payment-methods-for-a-payment", + "api-reference/payment-methods/list-payment-methods-for-a-customer" + ] + }, { "group": "Organization", "pages": [ @@ -119,8 +134,10 @@ "github": "https://github.com/juspay/hyperswitch", "linkedin": "https://www.linkedin.com/company/hyperswitch" }, - "openapi": ["openapi_spec.json"], + "openapi": [ + "openapi_spec.json" + ], "api": { "maintainOrder": true } -} +} \ No newline at end of file diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 15103188df5..2b66d1755ff 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -164,7 +164,7 @@ ] } }, - "/v2/organization/{id}/merchant_accounts": { + "/v2/organization/{id}/merchant-accounts": { "get": { "tags": [ "Organization" @@ -208,7 +208,7 @@ ] } }, - "/v2/connector_accounts": { + "/v2/connector-accounts": { "post": { "tags": [ "Merchant Connector Account" @@ -285,7 +285,7 @@ ] } }, - "/v2/connector_accounts/{id}": { + "/v2/connector-accounts/{id}": { "get": { "tags": [ "Merchant Connector Account" @@ -445,7 +445,7 @@ ] } }, - "/v2/merchant_accounts": { + "/v2/merchant-accounts": { "post": { "tags": [ "Merchant Account" @@ -524,7 +524,7 @@ ] } }, - "/v2/merchant_accounts/{id}": { + "/v2/merchant-accounts/{id}": { "get": { "tags": [ "Merchant Account" @@ -630,7 +630,7 @@ ] } }, - "/v2/merchant_accounts/{id}/profiles": { + "/v2/merchant-accounts/{id}/profiles": { "get": { "tags": [ "Merchant Account" @@ -907,7 +907,7 @@ ] } }, - "/v2/profiles/{id}/connector_accounts": { + "/v2/profiles/{id}/connector-accounts": { "get": { "tags": [ "Business Profile" @@ -966,7 +966,7 @@ ] } }, - "/v2/profiles/{id}/activate_routing_algorithm": { + "/v2/profiles/{id}/activate-routing-algorithm": { "patch": { "tags": [ "Profile" @@ -1033,7 +1033,7 @@ ] } }, - "/v2/profiles/{id}/deactivate_routing_algorithm": { + "/v2/profiles/{id}/deactivate-routing-algorithm": { "patch": { "tags": [ "Profile" @@ -1086,7 +1086,7 @@ ] } }, - "/v2/profiles/{id}/fallback_routing": { + "/v2/profiles/{id}/fallback-routing": { "patch": { "tags": [ "Profile" @@ -1197,13 +1197,13 @@ ] } }, - "/v2/profiles/{id}/routing_algorithm": { + "/v2/profiles/{id}/routing-algorithm": { "get": { "tags": [ "Profile" ], "summary": "Profile - Retrieve Active Routing Algorithm", - "description": "Retrieve active routing algorithm under the profile", + "description": "_\nRetrieve active routing algorithm under the profile", "operationId": "Retrieve the active routing algorithm under the profile", "parameters": [ { @@ -1271,7 +1271,7 @@ ] } }, - "/v2/routing_algorithm": { + "/v2/routing-algorithm": { "post": { "tags": [ "Routing" @@ -1326,7 +1326,7 @@ ] } }, - "/v2/routing_algorithm/{id}": { + "/v2/routing-algorithm/{id}": { "get": { "tags": [ "Routing" @@ -1376,7 +1376,7 @@ ] } }, - "/v2/api_keys": { + "/v2/api-keys": { "post": { "tags": [ "API Key" @@ -1416,7 +1416,7 @@ ] } }, - "/v2/api_keys/{id}": { + "/v2/api-keys/{id}": { "get": { "tags": [ "API Key" @@ -1545,7 +1545,7 @@ ] } }, - "/v2/api_keys/list": { + "/v2/api-keys/list": { "get": { "tags": [ "API Key" @@ -2017,6 +2017,332 @@ ] } }, + "/v2/payments/{id}/saved-payment-methods": { + "get": { + "tags": [ + "Payment Methods" + ], + "summary": "List customer saved payment methods for a payment", + "description": "To filter and list the applicable payment methods for a particular Customer ID, is to be associated with a payment", + "operationId": "List all Payment Methods for a Customer", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + }, + "404": { + "description": "Payment Methods does not exist in records" + } + }, + "security": [ + { + "publishable_key": [] + } + ] + } + }, + "/v2/customers/{id}/saved-payment-methods": { + "get": { + "tags": [ + "Payment Methods" + ], + "summary": "List saved payment methods for a Customer", + "description": "To filter and list the applicable payment methods for a particular Customer ID, to be used in a non-payments context", + "operationId": "List all Payment Methods for a Customer", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Methods retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + }, + "404": { + "description": "Payment Methods does not exist in records" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v2/payment-methods": { + "post": { + "tags": [ + "Payment Methods" + ], + "summary": "Payment Method - Create", + "description": "Creates and stores a payment method against a customer. In case of cards, this API should be used only by PCI compliant merchants.", + "operationId": "Create Payment Method", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Method Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v2/payment-methods/create-intent": { + "post": { + "tags": [ + "Payment Methods" + ], + "summary": "Payment Method - Create Intent", + "description": "Creates a payment method for customer with billing information and other metadata.", + "operationId": "Create Payment Method Intent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodIntentCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Method Intent Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v2/payment-methods/{id}/confirm-intent": { + "post": { + "tags": [ + "Payment Methods" + ], + "summary": "Payment Method - Confirm Intent", + "description": "Update a payment method with customer's payment method related information.", + "operationId": "Confirm Payment Method Intent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodIntentConfirm" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Method Intent Confirmed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v2/payment-methods/{id}/update-saved-payment-method": { + "patch": { + "tags": [ + "Payment Methods" + ], + "summary": "Payment Method - Update", + "description": "Update an existing payment method of a customer.", + "operationId": "Update Payment Method", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Method Update", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v2/payment-methods/{id}": { + "get": { + "tags": [ + "Payment Methods" + ], + "summary": "Payment Method - Retrieve", + "description": "Retrieves a payment method of a customer.", + "operationId": "Retrieve Payment Method", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The unique identifier for the Payment Method", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Payment Method Retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } + }, + "404": { + "description": "Payment Method Not Found" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Payment Methods" + ], + "summary": "Payment Method - Delete", + "description": "Deletes a payment method of a customer.", + "operationId": "Delete Payment Method", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The unique identifier for the Payment Method", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Payment Method Retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodDeleteResponse" + } + } + } + }, + "404": { + "description": "Payment Method Not Found" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/v2/refunds": { "post": { "tags": [ @@ -11255,14 +11581,17 @@ ], "properties": { "organization_name": { - "type": "string" + "type": "string", + "description": "Name of the organization" }, "organization_details": { "type": "object", + "description": "Details about the organization", "nullable": true }, "metadata": { "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true } }, @@ -11271,27 +11600,31 @@ "OrganizationResponse": { "type": "object", "required": [ - "organization_id", + "id", "modified_at", "created_at" ], "properties": { - "organization_id": { + "id": { "type": "string", + "description": "The unique identifier for the Organization", "example": "org_q98uSGAYbjEwqs0mJwnz", "maxLength": 64, "minLength": 1 }, "organization_name": { "type": "string", + "description": "Name of the Organization", "nullable": true }, "organization_details": { "type": "object", + "description": "Details about the organization", "nullable": true }, "metadata": { "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "modified_at": { @@ -11309,14 +11642,17 @@ "properties": { "organization_name": { "type": "string", + "description": "Name of the organization", "nullable": true }, "organization_details": { "type": "object", + "description": "Details about the organization", "nullable": true }, "metadata": { "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true } }, @@ -12918,6 +13254,68 @@ } } }, + "PaymentMethodIntentConfirm": { + "type": "object", + "required": [ + "client_secret", + "payment_method_data", + "payment_method_type", + "payment_method_subtype" + ], + "properties": { + "client_secret": { + "type": "string", + "description": "For SDK based calls, client_secret would be required" + }, + "customer_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 64, + "minLength": 1 + }, + "payment_method_data": { + "$ref": "#/components/schemas/PaymentMethodCreateData" + }, + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" + } + }, + "additionalProperties": false + }, + "PaymentMethodIntentCreate": { + "type": "object", + "required": [ + "customer_id" + ], + "properties": { + "metadata": { + "type": "object", + "description": "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.", + "nullable": true + }, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "customer_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64, + "minLength": 1 + } + }, + "additionalProperties": false + }, "PaymentMethodIssuerCode": { "type": "string", "enum": [ @@ -12959,6 +13357,78 @@ } ] }, + "PaymentMethodListRequest": { + "type": "object", + "properties": { + "client_secret": { + "type": "string", + "description": "This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK", + "example": "secret_k2uj3he2893eiu2d", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "accepted_countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "description": "The two-letter ISO currency code", + "example": [ + "US", + "UK", + "IN" + ], + "nullable": true + }, + "amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "accepted_currencies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + }, + "description": "The three-letter ISO currency code", + "example": [ + "USD", + "EUR" + ], + "nullable": true + }, + "recurring_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for recurring payments", + "example": true, + "nullable": true + }, + "card_networks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardNetwork" + }, + "description": "Indicates whether the payment method is eligible for card netwotks", + "example": [ + "visa", + "mastercard" + ], + "nullable": true + }, + "limit": { + "type": "integer", + "format": "int64", + "description": "Indicates the limit of last used payment methods", + "example": 1, + "nullable": true + } + }, + "additionalProperties": false + }, "PaymentMethodListResponse": { "type": "object", "required": [ diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index ecce327d7ff..d2133a3e68e 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -14432,14 +14432,17 @@ ], "properties": { "organization_name": { - "type": "string" + "type": "string", + "description": "Name of the organization" }, "organization_details": { "type": "object", + "description": "Details about the organization", "nullable": true }, "metadata": { "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true } }, @@ -14455,20 +14458,24 @@ "properties": { "organization_id": { "type": "string", + "description": "The unique identifier for the Organization", "example": "org_q98uSGAYbjEwqs0mJwnz", "maxLength": 64, "minLength": 1 }, "organization_name": { "type": "string", + "description": "Name of the Organization", "nullable": true }, "organization_details": { "type": "object", + "description": "Details about the organization", "nullable": true }, "metadata": { "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "modified_at": { @@ -14486,14 +14493,17 @@ "properties": { "organization_name": { "type": "string", + "description": "Name of the organization", "nullable": true }, "organization_details": { "type": "object", + "description": "Details about the organization", "nullable": true }, "metadata": { "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true } }, diff --git a/crates/api_models/src/organization.rs b/crates/api_models/src/organization.rs index f95a1595116..c6bc3924d11 100644 --- a/crates/api_models/src/organization.rs +++ b/crates/api_models/src/organization.rs @@ -22,9 +22,14 @@ pub struct OrganizationId { #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct OrganizationCreateRequest { + /// Name of the organization pub organization_name: String, + + /// Details about the organization #[schema(value_type = Option)] pub organization_details: Option, + + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option)] pub metadata: Option, } @@ -32,20 +37,53 @@ pub struct OrganizationCreateRequest { #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct OrganizationUpdateRequest { + /// Name of the organization pub organization_name: Option, + + /// Details about the organization #[schema(value_type = Option)] pub organization_details: Option, + + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option)] pub metadata: Option, } - +#[cfg(feature = "v1")] #[derive(Debug, serde::Serialize, Clone, ToSchema)] pub struct OrganizationResponse { + /// The unique identifier for the Organization #[schema(value_type = String, max_length = 64, min_length = 1, example = "org_q98uSGAYbjEwqs0mJwnz")] pub organization_id: id_type::OrganizationId, + + /// Name of the Organization pub organization_name: Option, + + /// Details about the organization #[schema(value_type = Option)] pub organization_details: Option, + + /// Metadata is useful for storing additional, unstructured information on an object. + #[schema(value_type = Option)] + pub metadata: Option, + pub modified_at: time::PrimitiveDateTime, + pub created_at: time::PrimitiveDateTime, +} + +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, Clone, ToSchema)] +pub struct OrganizationResponse { + /// The unique identifier for the Organization + #[schema(value_type = String, max_length = 64, min_length = 1, example = "org_q98uSGAYbjEwqs0mJwnz")] + pub id: id_type::OrganizationId, + + /// Name of the Organization + pub organization_name: Option, + + /// Details about the organization + #[schema(value_type = Option)] + pub organization_details: Option, + + /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option)] pub metadata: Option, pub modified_at: time::PrimitiveDateTime, diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 0bb5e65213f..2c2aa4861c5 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -778,7 +778,7 @@ pub struct PaymentMethodResponse { #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema, Clone)] pub struct PaymentMethodResponse { /// Unique identifier for a merchant - #[schema(example = "merchant_1671528864", value_type = String)] + #[schema(value_type = String, example = "merchant_1671528864")] pub merchant_id: id_type::MerchantId, /// The unique identifier of the customer. diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index b7a6c12500d..006d78e2f7a 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -125,7 +125,7 @@ impl PaymentIntent { publishable_key: String, ) -> CustomResult { let start_redirection_url = &format!( - "{}/v2/payments/{}/start_redirection?publishable_key={}&profile_id={}", + "{}/v2/payments/{}/start-redirection?publishable_key={}&profile_id={}", base_url, self.get_id().get_string_repr(), publishable_key, @@ -144,7 +144,7 @@ impl PaymentIntent { publishable_key: &str, ) -> CustomResult { let finish_redirection_url = format!( - "{base_url}/v2/payments/{}/finish_redirection/{publishable_key}/{}", + "{base_url}/v2/payments/{}/finish-redirection/{publishable_key}/{}", self.id.get_string_repr(), self.profile_id.get_string_repr() ); diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 4198e90882e..a756d9fb1b1 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -127,6 +127,17 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_confirm_intent, routes::payments::payment_status, + //Routes for payment methods + routes::payment_method::list_customer_payment_method_for_payment, + routes::payment_method::list_customer_payment_method_api, + routes::payment_method::create_payment_method_api, + routes::payment_method::create_payment_method_intent_api, + routes::payment_method::confirm_payment_method_intent_api, + routes::payment_method::payment_method_update_api, + routes::payment_method::payment_method_retrieve_api, + routes::payment_method::payment_method_delete_api, + + //Routes for refunds routes::refunds::refunds_create, ), @@ -170,9 +181,12 @@ Never share your secret api keys. Keep them guarded and secure. api_models::customers::CustomerRequest, api_models::customers::CustomerDeleteResponse, api_models::payment_methods::PaymentMethodCreate, + api_models::payment_methods::PaymentMethodIntentCreate, + api_models::payment_methods::PaymentMethodIntentConfirm, api_models::payment_methods::PaymentMethodResponse, api_models::payment_methods::PaymentMethodResponseData, api_models::payment_methods::CustomerPaymentMethod, + api_models::payment_methods::PaymentMethodListRequest, api_models::payment_methods::PaymentMethodListResponse, api_models::payment_methods::ResponsePaymentMethodsEnabled, api_models::payment_methods::ResponsePaymentMethodTypes, @@ -189,6 +203,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodCreateData, api_models::payment_methods::CardDetail, api_models::payment_methods::CardDetailUpdate, + api_models::payment_methods::CardType, api_models::payment_methods::RequestPaymentMethodTypes, api_models::payment_methods::CardType, api_models::payment_methods::PaymentMethodListData, diff --git a/crates/openapi/src/routes/api_keys.rs b/crates/openapi/src/routes/api_keys.rs index cfc4c09ce46..964fa60fcf5 100644 --- a/crates/openapi/src/routes/api_keys.rs +++ b/crates/openapi/src/routes/api_keys.rs @@ -25,7 +25,7 @@ pub async fn api_key_create() {} /// displayed only once on creation, so ensure you store it securely. #[utoipa::path( post, - path = "/v2/api_keys", + path = "/v2/api-keys", request_body= CreateApiKeyRequest, responses( (status = 200, description = "API Key created", body = CreateApiKeyResponse), @@ -64,7 +64,7 @@ pub async fn api_key_retrieve() {} /// Retrieve information about the specified API Key. #[utoipa::path( get, - path = "/v2/api_keys/{id}", + path = "/v2/api-keys/{id}", params ( ("id" = String, Path, description = "The unique identifier for the API Key") ), @@ -106,7 +106,7 @@ pub async fn api_key_update() {} /// Update information for the specified API Key. #[utoipa::path( put, - path = "/v2/api_keys/{id}", + path = "/v2/api-keys/{id}", request_body = UpdateApiKeyRequest, params ( ("id" = String, Path, description = "The unique identifier for the API Key") @@ -150,7 +150,7 @@ pub async fn api_key_revoke() {} /// authenticating with our APIs. #[utoipa::path( delete, - path = "/v2/api_keys/{id}", + path = "/v2/api-keys/{id}", params ( ("id" = String, Path, description = "The unique identifier for the API Key") ), @@ -191,7 +191,7 @@ pub async fn api_key_list() {} /// List all the API Keys associated to a merchant account. #[utoipa::path( get, - path = "/v2/api_keys/list", + path = "/v2/api-keys/list", params( ("limit" = Option, Query, description = "The maximum number of API Keys to include in the response"), ("skip" = Option, Query, description = "The number of API Keys to skip when retrieving the list of API keys."), diff --git a/crates/openapi/src/routes/merchant_account.rs b/crates/openapi/src/routes/merchant_account.rs index 022a5e6c006..a3bf96ab897 100644 --- a/crates/openapi/src/routes/merchant_account.rs +++ b/crates/openapi/src/routes/merchant_account.rs @@ -50,7 +50,7 @@ pub async fn merchant_account_create() {} /// Before creating the merchant account, it is mandatory to create an organization. #[utoipa::path( post, - path = "/v2/merchant_accounts", + path = "/v2/merchant-accounts", params( ( "X-Organization-Id" = String, Header, @@ -128,7 +128,7 @@ pub async fn retrieve_merchant_account() {} /// Retrieve a *merchant* account details. #[utoipa::path( get, - path = "/v2/merchant_accounts/{id}", + path = "/v2/merchant-accounts/{id}", params (("id" = String, Path, description = "The unique identifier for the merchant account")), responses( (status = 200, description = "Merchant Account Retrieved", body = MerchantAccountResponse), @@ -190,7 +190,7 @@ pub async fn update_merchant_account() {} /// Updates details of an existing merchant account. Helpful in updating merchant details such as email, contact details, or other configuration details like webhook, routing algorithm etc #[utoipa::path( put, - path = "/v2/merchant_accounts/{id}", + path = "/v2/merchant-accounts/{id}", request_body ( content = MerchantAccountUpdate, examples( @@ -300,7 +300,7 @@ pub async fn payment_connector_list_profile() {} /// List profiles for an Merchant #[utoipa::path( get, - path = "/v2/merchant_accounts/{id}/profiles", + path = "/v2/merchant-accounts/{id}/profiles", params (("id" = String, Path, description = "The unique identifier for the Merchant")), responses( (status = 200, description = "profile list retrieved successfully", body = Vec), diff --git a/crates/openapi/src/routes/merchant_connector_account.rs b/crates/openapi/src/routes/merchant_connector_account.rs index 29092b5bba0..372f8688a26 100644 --- a/crates/openapi/src/routes/merchant_connector_account.rs +++ b/crates/openapi/src/routes/merchant_connector_account.rs @@ -67,7 +67,7 @@ pub async fn connector_create() {} #[cfg(feature = "v2")] #[utoipa::path( post, - path = "/v2/connector_accounts", + path = "/v2/connector-accounts", request_body( content = MerchantConnectorCreate, examples( @@ -152,7 +152,7 @@ pub async fn connector_retrieve() {} #[cfg(feature = "v2")] #[utoipa::path( get, - path = "/v2/connector_accounts/{id}", + path = "/v2/connector-accounts/{id}", params( ("id" = i32, Path, description = "The unique identifier for the Merchant Connector") ), @@ -241,7 +241,7 @@ pub async fn connector_update() {} #[cfg(feature = "v2")] #[utoipa::path( put, - path = "/v2/connector_accounts/{id}", + path = "/v2/connector-accounts/{id}", request_body( content = MerchantConnectorUpdate, examples( @@ -310,7 +310,7 @@ pub async fn connector_delete() {} #[cfg(feature = "v2")] #[utoipa::path( delete, - path = "/v2/connector_accounts/{id}", + path = "/v2/connector-accounts/{id}", params( ("id" = i32, Path, description = "The unique identifier for the Merchant Connector") ), diff --git a/crates/openapi/src/routes/organization.rs b/crates/openapi/src/routes/organization.rs index ce3199343cf..d677131d5db 100644 --- a/crates/openapi/src/routes/organization.rs +++ b/crates/openapi/src/routes/organization.rs @@ -150,7 +150,7 @@ pub async fn organization_update() {} /// List merchant accounts for an Organization #[utoipa::path( get, - path = "/v2/organization/{id}/merchant_accounts", + path = "/v2/organization/{id}/merchant-accounts", params (("id" = String, Path, description = "The unique identifier for the Organization")), responses( (status = 200, description = "Merchant Account list retrieved successfully", body = Vec), diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index 3bc593aa5b2..b38a2342678 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -31,6 +31,7 @@ operation_id = "Create a Payment Method", security(("api_key" = [])) )] +#[cfg(feature = "v1")] pub async fn create_payment_method_api() {} /// List payment methods for a Merchant @@ -84,6 +85,7 @@ pub async fn list_payment_method_api() {} operation_id = "List all Payment Methods for a Customer", security(("api_key" = [])) )] +#[cfg(feature = "v1")] pub async fn list_customer_payment_method_api() {} /// List customer saved payment methods for a Payment @@ -130,6 +132,7 @@ pub async fn list_customer_payment_method_api_client() {} operation_id = "Retrieve a Payment method", security(("api_key" = [])) )] +#[cfg(feature = "v1")] pub async fn payment_method_retrieve_api() {} /// Payment Method - Update @@ -151,6 +154,7 @@ pub async fn payment_method_retrieve_api() {} operation_id = "Update a Payment method", security(("api_key" = []), ("publishable_key" = [])) )] +#[cfg(feature = "v1")] pub async fn payment_method_update_api() {} /// Payment Method - Delete @@ -170,6 +174,7 @@ pub async fn payment_method_update_api() {} operation_id = "Delete a Payment method", security(("api_key" = [])) )] +#[cfg(feature = "v1")] pub async fn payment_method_delete_api() {} /// Payment Method - Set Default Payment Method for Customer @@ -192,3 +197,171 @@ pub async fn payment_method_delete_api() {} security(("ephemeral_key" = [])) )] pub async fn default_payment_method_set_api() {} + +/// Payment Method - Create Intent +/// +/// Creates a payment method for customer with billing information and other metadata. +#[utoipa::path( + post, + path = "/v2/payment-methods/create-intent", + request_body( + content = PaymentMethodIntentCreate, + // TODO: Add examples + ), + responses( + (status = 200, description = "Payment Method Intent Created", body = PaymentMethodResponse), + (status = 400, description = "Invalid Data"), + ), + tag = "Payment Methods", + operation_id = "Create Payment Method Intent", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn create_payment_method_intent_api() {} + +/// Payment Method - Confirm Intent +/// +/// Update a payment method with customer's payment method related information. +#[utoipa::path( + post, + path = "/v2/payment-methods/{id}/confirm-intent", + request_body( + content = PaymentMethodIntentConfirm, + // TODO: Add examples + ), + responses( + (status = 200, description = "Payment Method Intent Confirmed", body = PaymentMethodResponse), + (status = 400, description = "Invalid Data"), + ), + tag = "Payment Methods", + operation_id = "Confirm Payment Method Intent", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn confirm_payment_method_intent_api() {} + +/// Payment Method - Create +/// +/// Creates and stores a payment method against a customer. In case of cards, this API should be used only by PCI compliant merchants. +#[utoipa::path( + post, + path = "/v2/payment-methods", + request_body( + content = PaymentMethodCreate, + // TODO: Add examples + ), + responses( + (status = 200, description = "Payment Method Created", body = PaymentMethodResponse), + (status = 400, description = "Invalid Data"), + ), + tag = "Payment Methods", + operation_id = "Create Payment Method", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn create_payment_method_api() {} + +/// Payment Method - Retrieve +/// +/// Retrieves a payment method of a customer. +#[utoipa::path( + get, + path = "/v2/payment-methods/{id}", + params ( + ("id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + responses( + (status = 200, description = "Payment Method Retrieved", body = PaymentMethodResponse), + (status = 404, description = "Payment Method Not Found"), + ), + tag = "Payment Methods", + operation_id = "Retrieve Payment Method", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn payment_method_retrieve_api() {} + +/// Payment Method - Update +/// +/// Update an existing payment method of a customer. +#[utoipa::path( + patch, + path = "/v2/payment-methods/{id}/update-saved-payment-method", + request_body( + content = PaymentMethodUpdate, + // TODO: Add examples + ), + responses( + (status = 200, description = "Payment Method Update", body = PaymentMethodResponse), + (status = 400, description = "Invalid Data"), + ), + tag = "Payment Methods", + operation_id = "Update Payment Method", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn payment_method_update_api() {} + +/// Payment Method - Delete +/// +/// Deletes a payment method of a customer. +#[utoipa::path( + delete, + path = "/v2/payment-methods/{id}", + params ( + ("id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + responses( + (status = 200, description = "Payment Method Retrieved", body = PaymentMethodDeleteResponse), + (status = 404, description = "Payment Method Not Found"), + ), + tag = "Payment Methods", + operation_id = "Delete Payment Method", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn payment_method_delete_api() {} + +/// List customer saved payment methods for a payment +/// +/// To filter and list the applicable payment methods for a particular Customer ID, is to be associated with a payment +#[utoipa::path( + get, + path = "/v2/payments/{id}/saved-payment-methods", + request_body( + content = PaymentMethodListRequest, + // TODO: Add examples and add param for customer_id + ), + responses( + (status = 200, description = "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("publishable_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn list_customer_payment_method_for_payment() {} + +/// List saved payment methods for a Customer +/// +/// To filter and list the applicable payment methods for a particular Customer ID, to be used in a non-payments context +#[utoipa::path( + get, + path = "/v2/customers/{id}/saved-payment-methods", + request_body( + content = PaymentMethodListRequest, + // TODO: Add examples and add param for customer_id + ), + responses( + (status = 200, description = "Payment Methods retrieved", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub async fn list_customer_payment_method_api() {} diff --git a/crates/openapi/src/routes/profile.rs b/crates/openapi/src/routes/profile.rs index d88568653a4..cc484aa3f95 100644 --- a/crates/openapi/src/routes/profile.rs +++ b/crates/openapi/src/routes/profile.rs @@ -210,7 +210,7 @@ pub async fn profile_update() {} /// Activates a routing algorithm under a profile #[utoipa::path( patch, - path = "/v2/profiles/{id}/activate_routing_algorithm", + path = "/v2/profiles/{id}/activate-routing-algorithm", request_body ( content = RoutingAlgorithmId, examples( ( "Activate a routing algorithm" = ( @@ -240,7 +240,7 @@ pub async fn routing_link_config() {} /// Deactivates a routing algorithm under a profile #[utoipa::path( patch, - path = "/v2/profiles/{id}/deactivate_routing_algorithm", + path = "/v2/profiles/{id}/deactivate-routing-algorithm", params( ("id" = String, Path, description = "The unique identifier for the profile"), ), @@ -263,7 +263,7 @@ pub async fn routing_unlink_config() {} /// Update the default fallback routing algorithm for the profile #[utoipa::path( patch, - path = "/v2/profiles/{id}/fallback_routing", + path = "/v2/profiles/{id}/fallback-routing", request_body = Vec, params( ("id" = String, Path, description = "The unique identifier for the profile"), @@ -307,11 +307,11 @@ pub async fn profile_retrieve() {} #[cfg(feature = "v2")] /// Profile - Retrieve Active Routing Algorithm -/// +///_ /// Retrieve active routing algorithm under the profile #[utoipa::path( get, - path = "/v2/profiles/{id}/routing_algorithm", + path = "/v2/profiles/{id}/routing-algorithm", params( ("id" = String, Path, description = "The unique identifier for the profile"), ("limit" = Option, Query, description = "The number of records of the algorithms to be returned"), @@ -334,7 +334,7 @@ pub async fn routing_retrieve_linked_config() {} /// Retrieve the default fallback routing algorithm for the profile #[utoipa::path( get, - path = "/v2/profiles/{id}/fallback_routing", + path = "/v2/profiles/{id}/fallback-routing", params( ("id" = String, Path, description = "The unique identifier for the profile"), ), @@ -353,7 +353,7 @@ pub async fn routing_retrieve_default_config() {} /// List Connector Accounts for the profile #[utoipa::path( get, - path = "/v2/profiles/{id}/connector_accounts", + path = "/v2/profiles/{id}/connector-accounts", params( ("id" = String, Path, description = "The unique identifier for the business profile"), ( diff --git a/crates/openapi/src/routes/routing.rs b/crates/openapi/src/routes/routing.rs index 67a22c2ca64..b144fd046ad 100644 --- a/crates/openapi/src/routes/routing.rs +++ b/crates/openapi/src/routes/routing.rs @@ -26,7 +26,7 @@ pub async fn routing_create_config() {} /// Create a routing algorithm #[utoipa::path( post, - path = "/v2/routing_algorithm", + path = "/v2/routing-algorithm", request_body = RoutingConfigRequest, responses( (status = 200, description = "Routing Algorithm created", body = RoutingDictionaryRecord), @@ -94,7 +94,7 @@ pub async fn routing_retrieve_config() {} #[utoipa::path( get, - path = "/v2/routing_algorithm/{id}", + path = "/v2/routing-algorithm/{id}", params( ("id" = String, Path, description = "The unique identifier for a routing algorithm"), ), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0f13e08d53e..bb0c547d7f1 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -556,11 +556,15 @@ impl Payments { ) .service(web::resource("").route(web::get().to(payments::payment_status))) .service( - web::resource("/start_redirection") + web::resource("/start-redirection") .route(web::get().to(payments::payments_start_redirection)), ) .service( - web::resource("/finish_redirection/{publishable_key}/{profile_id}") + web::resource("/saved-payment-methods") + .route(web::get().to(list_customer_payment_method_for_payment)), + ) + .service( + web::resource("/finish-redirection/{publishable_key}/{profile_id}") .route(web::get().to(payments::payments_finish_redirection)), ), ); @@ -715,7 +719,7 @@ pub struct Routing; #[cfg(all(feature = "olap", feature = "v2"))] impl Routing { pub fn server(state: AppState) -> Scope { - web::scope("/v2/routing_algorithm") + web::scope("/v2/routing-algorithm") .app_data(web::Data::new(state.clone())) .service( web::resource("").route(web::post().to(|state, req, payload| { @@ -968,7 +972,7 @@ impl Customers { #[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2"))] { route = route.service( - web::resource("/{customer_id}/saved_payment_methods") + web::resource("/{customer_id}/saved-payment-methods") .route(web::get().to(list_customer_payment_method_api)), ); } @@ -1113,7 +1117,7 @@ impl Payouts { #[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2",))] impl PaymentMethods { pub fn server(state: AppState) -> Scope { - let mut route = web::scope("/v2/payment_methods").app_data(web::Data::new(state)); + let mut route = web::scope("/v2/payment-methods").app_data(web::Data::new(state)); route = route .service(web::resource("").route(web::post().to(create_payment_method_api))) .service( @@ -1125,7 +1129,7 @@ impl PaymentMethods { .route(web::post().to(confirm_payment_method_intent_api)), ) .service( - web::resource("/{id}/update_saved_payment_method") + web::resource("/{id}/update-saved-payment-method") .route(web::patch().to(payment_method_update_api)), ) .service(web::resource("/{id}").route(web::get().to(payment_method_retrieve_api))) @@ -1267,7 +1271,7 @@ impl Organization { .route(web::put().to(admin::organization_update)), ) .service( - web::resource("/merchant_accounts") + web::resource("/merchant-accounts") .route(web::get().to(admin::merchant_account_list)), ), ) @@ -1279,7 +1283,7 @@ pub struct MerchantAccount; #[cfg(all(feature = "v2", feature = "olap"))] impl MerchantAccount { pub fn server(state: AppState) -> Scope { - web::scope("/v2/merchant_accounts") + web::scope("/v2/merchant-accounts") .app_data(web::Data::new(state)) .service(web::resource("").route(web::post().to(admin::merchant_account_create))) .service( @@ -1329,7 +1333,7 @@ pub struct MerchantConnectorAccount; #[cfg(all(any(feature = "olap", feature = "oltp"), feature = "v2"))] impl MerchantConnectorAccount { pub fn server(state: AppState) -> Scope { - let mut route = web::scope("/v2/connector_accounts").app_data(web::Data::new(state)); + let mut route = web::scope("/v2/connector-accounts").app_data(web::Data::new(state)); #[cfg(feature = "olap")] { @@ -1526,7 +1530,7 @@ pub struct ApiKeys; #[cfg(all(feature = "olap", feature = "v2"))] impl ApiKeys { pub fn server(state: AppState) -> Scope { - web::scope("/v2/api_keys") + web::scope("/v2/api-keys") .app_data(web::Data::new(state)) .service(web::resource("").route(web::post().to(api_keys::api_key_create))) .service(web::resource("/list").route(web::get().to(api_keys::api_key_list))) @@ -1691,16 +1695,16 @@ impl Profile { .route(web::put().to(profiles::profile_update)), ) .service( - web::resource("/connector_accounts") + web::resource("/connector-accounts") .route(web::get().to(admin::connector_list)), ) .service( - web::resource("/fallback_routing") + web::resource("/fallback-routing") .route(web::get().to(routing::routing_retrieve_default_config)) .route(web::patch().to(routing::routing_update_default_config)), ) .service( - web::resource("/activate_routing_algorithm").route(web::patch().to( + web::resource("/activate-routing-algorithm").route(web::patch().to( |state, req, path, payload| { routing::routing_link_config( state, @@ -1713,7 +1717,7 @@ impl Profile { )), ) .service( - web::resource("/deactivate_routing_algorithm").route(web::patch().to( + web::resource("/deactivate-routing-algorithm").route(web::patch().to( |state, req, path| { routing::routing_unlink_config( state, @@ -1724,7 +1728,7 @@ impl Profile { }, )), ) - .service(web::resource("/routing_algorithm").route(web::get().to( + .service(web::resource("/routing-algorithm").route(web::get().to( |state, req, query_params, path| { routing::routing_retrieve_linked_config( state, @@ -1999,7 +2003,7 @@ impl User { ) .service(web::resource("/verify_email").route(web::post().to(user::verify_email))) .service( - web::resource("/v2/verify_email").route(web::post().to(user::verify_email)), + web::resource("/v2/verify-email").route(web::post().to(user::verify_email)), ) .service( web::resource("/verify_email_request") @@ -2053,7 +2057,7 @@ impl User { .route(web::post().to(user_role::accept_invitations_v2)), ) .service( - web::resource("/pre_auth").route( + web::resource("/pre-auth").route( web::post().to(user_role::accept_invitations_pre_auth), ), ), diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 7296a510248..8ee31ecf943 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -508,30 +508,6 @@ pub async fn list_customer_payment_method_api( } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -/// List payment methods for a Customer v2 -/// -/// To filter and list the applicable payment methods for a particular Customer ID, is to be associated with a payment -#[utoipa::path( - get, - path = "v2/payments/{payment_id}/saved_payment_methods", - params ( - ("client-secret" = String, Path, description = "A secret known only to your application and the authorization server"), - ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), - ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), - ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), - ("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."), - ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), - ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), - ), - responses( - (status = 200, description = "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", body = CustomerPaymentMethodsListResponse), - (status = 400, description = "Invalid Data"), - (status = 404, description = "Payment Methods does not exist in records") - ), - tag = "Payment Methods", - operation_id = "List all Payment Methods for a Customer", - security(("publishable_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))] pub async fn list_customer_payment_method_for_payment( state: web::Data, @@ -575,29 +551,6 @@ pub async fn list_customer_payment_method_for_payment( feature = "payment_methods_v2", feature = "customer_v2" ))] -/// List payment methods for a Customer v2 -/// -/// To filter and list the applicable payment methods for a particular Customer ID, to be used in a non-payments context -#[utoipa::path( - get, - path = "v2/customers/{customer_id}/saved_payment_methods", - params ( - ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), - ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), - ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), - ("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."), - ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), - ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), - ), - responses( - (status = 200, description = "Payment Methods retrieved", body = CustomerPaymentMethodsListResponse), - (status = 400, description = "Invalid Data"), - (status = 404, description = "Payment Methods does not exist in records") - ), - tag = "Payment Methods", - operation_id = "List all Payment Methods for a Customer", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))] pub async fn list_customer_payment_method_api( state: web::Data, diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index c81fc7ceb48..85275a768df 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -34,6 +34,10 @@ use crate::{ impl ForeignFrom for OrganizationResponse { fn foreign_from(org: diesel_models::organization::Organization) -> Self { Self { + #[cfg(feature = "v2")] + id: org.get_organization_id(), + + #[cfg(feature = "v1")] organization_id: org.get_organization_id(), organization_name: org.get_organization_name(), organization_details: org.organization_details, diff --git a/cypress-tests-v2/cypress/support/commands.js b/cypress-tests-v2/cypress/support/commands.js index eb4ca3423eb..b6955c542a7 100644 --- a/cypress-tests-v2/cypress/support/commands.js +++ b/cypress-tests-v2/cypress/support/commands.js @@ -64,10 +64,10 @@ Cypress.Commands.add( if (response.status === 200) { expect(response.body) - .to.have.property("organization_id") + .to.have.property("id") .and.to.include("org_") .and.to.be.a("string").and.not.be.empty; - globalState.set("organizationId", response.body.organization_id); + globalState.set("organizationId", response.body.id); cy.task("setGlobalState", globalState.data); expect(response.body).to.have.property("metadata").and.to.equal(null); } else { @@ -99,7 +99,7 @@ Cypress.Commands.add("organizationRetrieveCall", (globalState) => { if (response.status === 200) { expect(response.body) - .to.have.property("organization_id") + .to.have.property("id") .and.to.include("org_") .and.to.be.a("string").and.not.be.empty; expect(response.body.organization_name) @@ -107,7 +107,7 @@ Cypress.Commands.add("organizationRetrieveCall", (globalState) => { .and.to.be.a("string").and.not.be.empty; if (organization_id === undefined || organization_id === null) { - globalState.set("organizationId", response.body.organization_id); + globalState.set("organizationId", response.body.id); cy.task("setGlobalState", globalState.data); } } else { @@ -144,14 +144,14 @@ Cypress.Commands.add( if (response.status === 200) { expect(response.body) - .to.have.property("organization_id") + .to.have.property("id") .and.to.include("org_") .and.to.be.a("string").and.not.be.empty; expect(response.body).to.have.property("metadata").and.to.be.a("object") .and.not.be.empty; if (organization_id === undefined || organization_id === null) { - globalState.set("organizationId", response.body.organization_id); + globalState.set("organizationId", response.body.id); cy.task("setGlobalState", globalState.data); } } else { @@ -174,7 +174,7 @@ Cypress.Commands.add( const key_id_type = "publishable_key"; const key_id = validateEnv(base_url, key_id_type); const organization_id = globalState.get("organizationId"); - const url = `${base_url}/v2/merchant_accounts`; + const url = `${base_url}/v2/merchant-accounts`; const merchant_name = merchantAccountCreateBody.merchant_name .replaceAll(" ", "") @@ -223,7 +223,7 @@ Cypress.Commands.add("merchantAccountRetrieveCall", (globalState) => { const key_id_type = "publishable_key"; const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); - const url = `${base_url}/v2/merchant_accounts/${merchant_id}`; + const url = `${base_url}/v2/merchant-accounts/${merchant_id}`; cy.request({ method: "GET", @@ -265,7 +265,7 @@ Cypress.Commands.add( const key_id_type = "publishable_key"; const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); - const url = `${base_url}/v2/merchant_accounts/${merchant_id}`; + const url = `${base_url}/v2/merchant-accounts/${merchant_id}`; const merchant_name = merchantAccountUpdateBody.merchant_name; @@ -456,7 +456,7 @@ Cypress.Commands.add( const base_url = globalState.get("baseUrl"); const merchant_id = globalState.get("merchantId"); const profile_id = globalState.get("profileId"); - const url = `${base_url}/v2/connector_accounts`; + const url = `${base_url}/v2/connector-accounts`; const customHeaders = { "x-merchant-id": merchant_id, @@ -536,7 +536,7 @@ Cypress.Commands.add("mcaRetrieveCall", (globalState) => { const connector_name = globalState.get("connectorId"); const merchant_connector_id = globalState.get("merchantConnectorId"); const merchant_id = globalState.get("merchantId"); - const url = `${base_url}/v2/connector_accounts/${merchant_connector_id}`; + const url = `${base_url}/v2/connector-accounts/${merchant_connector_id}`; const customHeaders = { "x-merchant-id": merchant_id, @@ -590,7 +590,7 @@ Cypress.Commands.add( const merchant_connector_id = globalState.get("merchantConnectorId"); const merchant_id = globalState.get("merchantId"); const profile_id = globalState.get("profileId"); - const url = `${base_url}/v2/connector_accounts/${merchant_connector_id}`; + const url = `${base_url}/v2/connector-accounts/${merchant_connector_id}`; const customHeaders = { "x-merchant-id": merchant_id, @@ -653,7 +653,7 @@ Cypress.Commands.add("apiKeyCreateCall", (apiKeyCreateBody, globalState) => { const key_id_type = "key_id"; const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); - const url = `${base_url}/v2/api_keys`; + const url = `${base_url}/v2/api-keys`; const customHeaders = { "x-merchant-id": merchant_id, @@ -703,7 +703,7 @@ Cypress.Commands.add("apiKeyRetrieveCall", (globalState) => { const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); const api_key_id = globalState.get("apiKeyId"); - const url = `${base_url}/v2/api_keys/${api_key_id}`; + const url = `${base_url}/v2/api-keys/${api_key_id}`; const customHeaders = { "x-merchant-id": merchant_id, @@ -750,7 +750,7 @@ Cypress.Commands.add("apiKeyUpdateCall", (apiKeyUpdateBody, globalState) => { const key_id_type = "key_id"; const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); - const url = `${base_url}/v2/api_keys/${api_key_id}`; + const url = `${base_url}/v2/api-keys/${api_key_id}`; const customHeaders = { "x-merchant-id": merchant_id, @@ -801,7 +801,7 @@ Cypress.Commands.add( const api_key = globalState.get("userInfoToken"); const base_url = globalState.get("baseUrl"); const profile_id = globalState.get("profileId"); - const url = `${base_url}/v2/routing_algorithm`; + const url = `${base_url}/v2/routing-algorithm`; // Update request body routingSetupBody.algorithm.data = payload.data; @@ -847,7 +847,7 @@ Cypress.Commands.add( const base_url = globalState.get("baseUrl"); const profile_id = globalState.get("profileId"); const routing_algorithm_id = globalState.get("routingAlgorithmId"); - const url = `${base_url}/v2/profiles/${profile_id}/activate_routing_algorithm`; + const url = `${base_url}/v2/profiles/${profile_id}/activate-routing-algorithm`; // Update request body routingActivationBody.routing_algorithm_id = routing_algorithm_id; @@ -885,7 +885,7 @@ Cypress.Commands.add("routingActivationRetrieveCall", (globalState) => { const profile_id = globalState.get("profileId"); const query_params = "limit=10"; const routing_algorithm_id = globalState.get("routingAlgorithmId"); - const url = `${base_url}/v2/profiles/${profile_id}/routing_algorithm?${query_params}`; + const url = `${base_url}/v2/profiles/${profile_id}/routing-algorithm?${query_params}`; cy.request({ method: "GET", @@ -922,7 +922,7 @@ Cypress.Commands.add("routingDeactivateCall", (globalState) => { const base_url = globalState.get("baseUrl"); const profile_id = globalState.get("profileId"); const routing_algorithm_id = globalState.get("routingAlgorithmId"); - const url = `${base_url}/v2/profiles/${profile_id}/deactivate_routing_algorithm`; + const url = `${base_url}/v2/profiles/${profile_id}/deactivate-routing-algorithm`; cy.request({ method: "PATCH", @@ -957,7 +957,7 @@ Cypress.Commands.add("routingRetrieveCall", (globalState) => { const base_url = globalState.get("baseUrl"); const profile_id = globalState.get("profileId"); const routing_algorithm_id = globalState.get("routingAlgorithmId"); - const url = `${base_url}/v2/routing_algorithm/${routing_algorithm_id}`; + const url = `${base_url}/v2/routing-algorithm/${routing_algorithm_id}`; cy.request({ method: "GET", @@ -996,7 +996,7 @@ Cypress.Commands.add( const base_url = globalState.get("baseUrl"); const profile_id = globalState.get("profileId"); const routing_algorithm_id = globalState.get("routingAlgorithmId"); - const url = `${base_url}/v2/profiles/${profile_id}/fallback_routing`; + const url = `${base_url}/v2/profiles/${profile_id}/fallback-routing`; // Update request body routingDefaultFallbackBody = payload; @@ -1029,7 +1029,7 @@ Cypress.Commands.add("routingFallbackRetrieveCall", (globalState) => { const api_key = globalState.get("userInfoToken"); const base_url = globalState.get("baseUrl"); const profile_id = globalState.get("profileId"); - const url = `${base_url}/v2/profiles/${profile_id}/fallback_routing`; + const url = `${base_url}/v2/profiles/${profile_id}/fallback-routing`; cy.request({ method: "GET", @@ -1166,7 +1166,7 @@ Cypress.Commands.add("merchantAccountsListCall", (globalState) => { const key_id_type = "publishable_key"; const key_id = validateEnv(base_url, key_id_type); const organization_id = globalState.get("organizationId"); - const url = `${base_url}/v2/organization/${organization_id}/merchant_accounts`; + const url = `${base_url}/v2/organization/${organization_id}/merchant-accounts`; cy.request({ method: "GET", @@ -1204,7 +1204,7 @@ Cypress.Commands.add("businessProfilesListCall", (globalState) => { const api_key = globalState.get("adminApiKey"); const base_url = globalState.get("baseUrl"); const merchant_id = globalState.get("merchantId"); - const url = `${base_url}/v2/merchant_accounts/${merchant_id}/profiles`; + const url = `${base_url}/v2/merchant-accounts/${merchant_id}/profiles`; const customHeaders = { "x-merchant-id": merchant_id, @@ -1246,7 +1246,7 @@ Cypress.Commands.add("mcaListCall", (globalState, service_type) => { const base_url = globalState.get("baseUrl"); const merchant_id = globalState.get("merchantId"); const profile_id = globalState.get("profileId"); - const url = `${base_url}/v2/profiles/${profile_id}/connector_accounts`; + const url = `${base_url}/v2/profiles/${profile_id}/connector-accounts`; const customHeaders = { "x-merchant-id": merchant_id, @@ -1308,7 +1308,7 @@ Cypress.Commands.add("apiKeysListCall", (globalState) => { const key_id_type = "key_id"; const key_id = validateEnv(base_url, key_id_type); const merchant_id = globalState.get("merchantId"); - const url = `${base_url}/v2/api_keys/list`; + const url = `${base_url}/v2/api-keys/list`; const customHeaders = { "x-merchant-id": merchant_id, From 55fe82fdcd78df9608842190f1423088740d1087 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:42:09 +0530 Subject: [PATCH 46/51] refactor(users): Use domain email type in user DB functions (#6699) --- .../src/errors/api_error_response.rs | 8 ++++ .../router/src/compatibility/stripe/errors.rs | 3 +- crates/router/src/core/user.rs | 44 ++++++------------- crates/router/src/core/user_role.rs | 2 +- crates/router/src/db/kafka_store.rs | 6 +-- crates/router/src/db/user.rs | 23 +++++----- crates/router/src/services/authentication.rs | 14 +++--- .../src/services/authentication/cookies.rs | 6 +-- crates/router/src/services/authorization.rs | 10 +++-- crates/router/src/services/email/types.rs | 11 +++-- crates/router/src/types/domain/user.rs | 6 ++- crates/router/src/utils/user.rs | 2 +- 12 files changed, 68 insertions(+), 67 deletions(-) diff --git a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs index b02da396075..850cc470316 100644 --- a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs +++ b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs @@ -274,6 +274,11 @@ pub enum ApiErrorResponse { LinkConfigurationError { message: String }, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_41", message = "Payout validation failed")] PayoutFailed { data: Option }, + #[error( + error_type = ErrorType::InvalidRequestError, code = "IR_42", + message = "Cookies are not found in the request" + )] + CookieNotFound, #[error(error_type = ErrorType::InvalidRequestError, code = "WE_01", message = "Failed to authenticate the webhook")] WebhookAuthenticationFailed, @@ -627,6 +632,9 @@ impl ErrorSwitch for ApiErrorRespon Self::PayoutFailed { data } => { AER::BadRequest(ApiError::new("IR", 41, "Payout failed while processing with connector.", Some(Extra { data: data.clone(), ..Default::default()}))) }, + Self::CookieNotFound => { + AER::Unauthorized(ApiError::new("IR", 42, "Cookies are not found in the request", None)) + }, Self::WebhookAuthenticationFailed => { AER::Unauthorized(ApiError::new("WE", 1, "Webhook authentication failed", None)) diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index efe0ac157e0..6cf078b5f81 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -444,7 +444,8 @@ impl From for StripeErrorCode { | errors::ApiErrorResponse::GenericUnauthorized { .. } | errors::ApiErrorResponse::AccessForbidden { .. } | errors::ApiErrorResponse::InvalidCookie - | errors::ApiErrorResponse::InvalidEphemeralKey => Self::Unauthorized, + | errors::ApiErrorResponse::InvalidEphemeralKey + | errors::ApiErrorResponse::CookieNotFound => Self::Unauthorized, errors::ApiErrorResponse::InvalidRequestUrl | errors::ApiErrorResponse::InvalidHttpMethod | errors::ApiErrorResponse::InvalidCardIin diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 22623c4ca66..1168ea87bf5 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -166,7 +166,7 @@ pub async fn signin_token_only_flow( ) -> UserResponse { let user_from_db: domain::UserFromStorage = state .global_store - .find_user_by_email(&request.email) + .find_user_by_email(&domain::UserEmail::from_pii_email(request.email)?) .await .to_not_found_response(UserErrors::InvalidCredentials)? .into(); @@ -191,7 +191,10 @@ pub async fn connect_account( request: user_api::ConnectAccountRequest, auth_id: Option, ) -> UserResponse { - let find_user = state.global_store.find_user_by_email(&request.email).await; + let find_user = state + .global_store + .find_user_by_email(&domain::UserEmail::from_pii_email(request.email.clone())?) + .await; if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); @@ -369,7 +372,7 @@ pub async fn forgot_password( let user_from_db = state .global_store - .find_user_by_email(&user_email.into_inner()) + .find_user_by_email(&user_email) .await .map_err(|e| { if e.current_context().is_db_not_found() { @@ -453,11 +456,7 @@ pub async fn reset_password_token_only_flow( let user_from_db: domain::UserFromStorage = state .global_store - .find_user_by_email( - &email_token - .get_email() - .change_context(UserErrors::InternalServerError)?, - ) + .find_user_by_email(&email_token.get_email()?) .await .change_context(UserErrors::InternalServerError)? .into(); @@ -564,10 +563,7 @@ async fn handle_invitation( } let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; - let invitee_user = state - .global_store - .find_user_by_email(&invitee_email.into_inner()) - .await; + let invitee_user = state.global_store.find_user_by_email(&invitee_email).await; if let Ok(invitee_user) = invitee_user { handle_existing_user_invitation( @@ -958,7 +954,7 @@ pub async fn resend_invite( let invitee_email = domain::UserEmail::from_pii_email(request.email)?; let user: domain::UserFromStorage = state .global_store - .find_user_by_email(&invitee_email.clone().into_inner()) + .find_user_by_email(&invitee_email) .await .map_err(|e| { if e.current_context().is_db_not_found() { @@ -1065,11 +1061,7 @@ pub async fn accept_invite_from_email_token_only_flow( let user_from_db: domain::UserFromStorage = state .global_store - .find_user_by_email( - &email_token - .get_email() - .change_context(UserErrors::InternalServerError)?, - ) + .find_user_by_email(&email_token.get_email()?) .await .change_context(UserErrors::InternalServerError)? .into(); @@ -1525,11 +1517,7 @@ pub async fn verify_email_token_only_flow( let user_from_email = state .global_store - .find_user_by_email( - &email_token - .get_email() - .change_context(UserErrors::InternalServerError)?, - ) + .find_user_by_email(&email_token.get_email()?) .await .change_context(UserErrors::InternalServerError)?; @@ -1572,7 +1560,7 @@ pub async fn send_verification_mail( let user_email = domain::UserEmail::try_from(req.email)?; let user = state .global_store - .find_user_by_email(&user_email.into_inner()) + .find_user_by_email(&user_email) .await .map_err(|e| { if e.current_context().is_db_not_found() { @@ -1669,11 +1657,7 @@ pub async fn user_from_email( let user_from_db: domain::UserFromStorage = state .global_store - .find_user_by_email( - &email_token - .get_email() - .change_context(UserErrors::InternalServerError)?, - ) + .find_user_by_email(&email_token.get_email()?) .await .change_context(UserErrors::InternalServerError)? .into(); @@ -2379,7 +2363,7 @@ pub async fn sso_sign( // TODO: Use config to handle not found error let user_from_db: domain::UserFromStorage = state .global_store - .find_user_by_email(&email.into_inner()) + .find_user_by_email(&email) .await .map(Into::into) .to_not_found_response(UserErrors::UserNotFound)?; diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index eaa655a07f3..273b863afe8 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -472,7 +472,7 @@ pub async fn delete_user_role( ) -> UserResponse<()> { let user_from_db: domain::UserFromStorage = state .global_store - .find_user_by_email(&domain::UserEmail::from_pii_email(request.email)?.into_inner()) + .find_user_by_email(&domain::UserEmail::from_pii_email(request.email)?) .await .map_err(|e| { if e.current_context().is_db_not_found() { diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 436755ea720..ec45d9e18d7 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use common_enums::enums::MerchantStorageScheme; use common_utils::{ errors::CustomResult, - id_type, pii, + id_type, types::{keymanager::KeyManagerState, theme::ThemeLineage}, }; use diesel_models::{ @@ -2983,7 +2983,7 @@ impl UserInterface for KafkaStore { async fn find_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, ) -> CustomResult { self.diesel_store.find_user_by_email(user_email).await } @@ -3007,7 +3007,7 @@ impl UserInterface for KafkaStore { async fn update_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, user: storage::UserUpdate, ) -> CustomResult { self.diesel_store diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index 14bed15fa45..089c9fc6eb7 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -3,11 +3,10 @@ use error_stack::report; use masking::Secret; use router_env::{instrument, tracing}; -use super::MockDb; +use super::{domain, MockDb}; use crate::{ connection, core::errors::{self, CustomResult}, - pii, services::Store, }; pub mod sample_data; @@ -22,7 +21,7 @@ pub trait UserInterface { async fn find_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, ) -> CustomResult; async fn find_user_by_id( @@ -38,7 +37,7 @@ pub trait UserInterface { async fn update_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, user: storage::UserUpdate, ) -> CustomResult; @@ -70,10 +69,10 @@ impl UserInterface for Store { #[instrument(skip_all)] async fn find_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; - storage::User::find_by_user_email(&conn, user_email) + storage::User::find_by_user_email(&conn, user_email.get_inner()) .await .map_err(|error| report!(errors::StorageError::from(error))) } @@ -104,11 +103,11 @@ impl UserInterface for Store { #[instrument(skip_all)] async fn update_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, user: storage::UserUpdate, ) -> CustomResult { let conn = connection::pg_connection_write(self).await?; - storage::User::update_by_user_email(&conn, user_email, user) + storage::User::update_by_user_email(&conn, user_email.get_inner(), user) .await .map_err(|error| report!(errors::StorageError::from(error))) } @@ -171,12 +170,12 @@ impl UserInterface for MockDb { async fn find_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, ) -> CustomResult { let users = self.users.lock().await; users .iter() - .find(|user| user.email.eq(user_email)) + .find(|user| user.email.eq(user_email.get_inner())) .cloned() .ok_or( errors::StorageError::ValueNotFound(format!( @@ -253,13 +252,13 @@ impl UserInterface for MockDb { async fn update_user_by_email( &self, - user_email: &pii::Email, + user_email: &domain::UserEmail, update_user: storage::UserUpdate, ) -> CustomResult { let mut users = self.users.lock().await; users .iter_mut() - .find(|user| user.email.eq(user_email)) + .find(|user| user.email.eq(user_email.get_inner())) .map(|user| { *user = match &update_user { storage::UserUpdate::VerifyUser => storage::User { diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index d50933b708d..724539ad08a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -2548,7 +2548,8 @@ where T: serde::de::DeserializeOwned, A: SessionStateInfo + Sync, { - let cookie_token_result = get_cookie_from_header(headers).and_then(cookies::parse_cookie); + let cookie_token_result = + get_cookie_from_header(headers).and_then(cookies::get_jwt_from_cookies); let auth_header_token_result = get_jwt_from_authorization_header(headers); let force_cookie = state.conf().user.force_cookies; @@ -3115,7 +3116,7 @@ pub fn is_ephemeral_auth( pub fn is_jwt_auth(headers: &HeaderMap) -> bool { headers.get(headers::AUTHORIZATION).is_some() || get_cookie_from_header(headers) - .and_then(cookies::parse_cookie) + .and_then(cookies::get_jwt_from_cookies) .is_ok() } @@ -3177,10 +3178,13 @@ pub fn get_jwt_from_authorization_header(headers: &HeaderMap) -> RouterResult<&s } pub fn get_cookie_from_header(headers: &HeaderMap) -> RouterResult<&str> { - headers + let cookie = headers .get(cookies::get_cookie_header()) - .and_then(|header_value| header_value.to_str().ok()) - .ok_or(errors::ApiErrorResponse::InvalidCookie.into()) + .ok_or(report!(errors::ApiErrorResponse::CookieNotFound))?; + + cookie + .to_str() + .change_context(errors::ApiErrorResponse::InvalidCookie) } pub fn strip_jwt_token(token: &str) -> RouterResult<&str> { diff --git a/crates/router/src/services/authentication/cookies.rs b/crates/router/src/services/authentication/cookies.rs index 96518840800..4a297c0f0eb 100644 --- a/crates/router/src/services/authentication/cookies.rs +++ b/crates/router/src/services/authentication/cookies.rs @@ -49,7 +49,7 @@ pub fn remove_cookie_response() -> UserResponse<()> { Ok(ApplicationResponse::JsonWithHeaders(((), header))) } -pub fn parse_cookie(cookies: &str) -> RouterResult { +pub fn get_jwt_from_cookies(cookies: &str) -> RouterResult { Cookie::split_parse(cookies) .find_map(|cookie| { cookie @@ -57,8 +57,8 @@ pub fn parse_cookie(cookies: &str) -> RouterResult { .filter(|parsed_cookie| parsed_cookie.name() == JWT_TOKEN_COOKIE_NAME) .map(|parsed_cookie| parsed_cookie.value().to_owned()) }) - .ok_or(report!(ApiErrorResponse::InvalidCookie)) - .attach_printable("Cookie Parsing Failed") + .ok_or(report!(ApiErrorResponse::InvalidJwtToken)) + .attach_printable("Unable to find JWT token in cookies") } #[cfg(feature = "olap")] diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs index 87ff9f6abd5..8f48ef068ce 100644 --- a/crates/router/src/services/authorization.rs +++ b/crates/router/src/services/authorization.rs @@ -40,10 +40,12 @@ where i64::try_from(token.exp).change_context(ApiErrorResponse::InternalServerError)?; let cache_ttl = token_expiry - common_utils::date_time::now_unix_timestamp(); - set_role_info_in_cache(state, &token.role_id, &role_info, cache_ttl) - .await - .map_err(|e| logger::error!("Failed to set role info in cache {e:?}")) - .ok(); + if cache_ttl > 0 { + set_role_info_in_cache(state, &token.role_id, &role_info, cache_ttl) + .await + .map_err(|e| logger::error!("Failed to set role info in cache {e:?}")) + .ok(); + } Ok(role_info) } diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index d092afdc5de..8c966d79d80 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -1,9 +1,6 @@ use api_models::user::dashboard_metadata::ProdIntent; use common_enums::EntityType; -use common_utils::{ - errors::{self, CustomResult}, - pii, -}; +use common_utils::{errors::CustomResult, pii}; use error_stack::ResultExt; use external_services::email::{EmailContents, EmailData, EmailError}; use masking::{ExposeInterface, Secret}; @@ -183,7 +180,7 @@ impl EmailToken { entity: Option, flow: domain::Origin, settings: &configs::Settings, - ) -> CustomResult { + ) -> UserResult { let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(expiration_duration)?.as_secs(); let token_payload = Self { @@ -195,8 +192,10 @@ impl EmailToken { jwt::generate_jwt(&token_payload, settings).await } - pub fn get_email(&self) -> CustomResult { + pub fn get_email(&self) -> UserResult { pii::Email::try_from(self.email.clone()) + .change_context(UserErrors::InternalServerError) + .and_then(domain::UserEmail::from_pii_email) } pub fn get_entity(&self) -> Option<&Entity> { diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 6d0d2a4ea07..21212bf6b62 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -131,6 +131,10 @@ impl UserEmail { self.0 } + pub fn get_inner(&self) -> &pii::Email { + &self.0 + } + pub fn get_secret(self) -> Secret { (*self.0).clone() } @@ -617,7 +621,7 @@ impl NewUser { pub async fn check_if_already_exists_in_db(&self, state: SessionState) -> UserResult<()> { if state .global_store - .find_user_by_email(&self.get_email().into_inner()) + .find_user_by_email(&self.get_email()) .await .is_ok() { diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index f115a16c062..443db741ae3 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -124,7 +124,7 @@ pub async fn get_user_from_db_by_email( ) -> CustomResult { state .global_store - .find_user_by_email(&email.into_inner()) + .find_user_by_email(&email) .await .map(UserFromStorage::from) } From 22b5a93e02156cd92adf5e24a3fd8d0e39e8d429 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:23:14 +0000 Subject: [PATCH 47/51] chore(version): 2024.12.02.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a215cbe0557..454a5545687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.02.0 + +### Features + +- **connector:** + - [Adyen] Fetch email from customer email for payment request ([#6676](https://github.com/juspay/hyperswitch/pull/6676)) ([`9998c55`](https://github.com/juspay/hyperswitch/commit/9998c557c9c88496ffbee883e7fc4b76614cff50)) + - [REDSYS] add Connector Template Code ([#6659](https://github.com/juspay/hyperswitch/pull/6659)) ([`19cbcdd`](https://github.com/juspay/hyperswitch/commit/19cbcdd979bb74119d80c37c313fd0ffeb58bb8d)) +- **payments:** [Payment links] add showCardFormByDefault config for payment links ([#6663](https://github.com/juspay/hyperswitch/pull/6663)) ([`b1d1073`](https://github.com/juspay/hyperswitch/commit/b1d1073389f58c480a53a27be24aa91554520ff1)) +- **users:** Add tenant id reads in user roles ([#6661](https://github.com/juspay/hyperswitch/pull/6661)) ([`9212f77`](https://github.com/juspay/hyperswitch/commit/9212f77684b04115332d9be5c3d20bdc56b02160)) + +### Bug Fixes + +- **analytics:** Fix first_attempt filter value parsing for Payments ([#6667](https://github.com/juspay/hyperswitch/pull/6667)) ([`abcaa53`](https://github.com/juspay/hyperswitch/commit/abcaa539eccdae86c7a68fd4ce60ab9889f9fb43)) +- **openapi:** Standardise API naming scheme for V2 ([#6510](https://github.com/juspay/hyperswitch/pull/6510)) ([`96393ff`](https://github.com/juspay/hyperswitch/commit/96393ff3d6b11d4726a6cb2224236414507d9848)) +- **opensearch:** Handle empty free-text query search in global search ([#6685](https://github.com/juspay/hyperswitch/pull/6685)) ([`b1cdff0`](https://github.com/juspay/hyperswitch/commit/b1cdff0950f32b38e3ff0eeac2b726ba0f671051)) +- **router:** Populate card network in the network transaction id based MIT flow ([#6690](https://github.com/juspay/hyperswitch/pull/6690)) ([`6a20701`](https://github.com/juspay/hyperswitch/commit/6a2070172b8d845e6db36b7789defddf8ea4e1e9)) +- **users:** Mark user as verified if user logins from SSO ([#6694](https://github.com/juspay/hyperswitch/pull/6694)) ([`880ad1e`](https://github.com/juspay/hyperswitch/commit/880ad1e883fb42f73c2805287e64bc2c2dcbb9f3)) + +### Refactors + +- **currency_conversion:** Release redis lock if api call fails ([#6671](https://github.com/juspay/hyperswitch/pull/6671)) ([`ae7d16e`](https://github.com/juspay/hyperswitch/commit/ae7d16e23699c8ed95a7e2eab7539cfe20f847d0)) +- **router:** [ZSL] remove partially capture status ([#6689](https://github.com/juspay/hyperswitch/pull/6689)) ([`0572626`](https://github.com/juspay/hyperswitch/commit/05726262e6a3f6fcb18c0dbe41c18e4d6e84608b)) +- **users:** Use domain email type in user DB functions ([#6699](https://github.com/juspay/hyperswitch/pull/6699)) ([`55fe82f`](https://github.com/juspay/hyperswitch/commit/55fe82fdcd78df9608842190f1423088740d1087)) + +**Full Changelog:** [`2024.11.29.0...2024.12.02.0`](https://github.com/juspay/hyperswitch/compare/2024.11.29.0...2024.12.02.0) + +- - - + ## 2024.11.29.0 ### Features From b097d7f5a984b32421494ea033029d01d034fab8 Mon Sep 17 00:00:00 2001 From: Anurag Thakur Date: Mon, 2 Dec 2024 14:59:35 +0530 Subject: [PATCH 48/51] fix(openapi): Revert Standardise API naming scheme for V2 Dashboard Changes (#6712) --- crates/router/src/routes/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index bb0c547d7f1..7a8571479f4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2003,7 +2003,7 @@ impl User { ) .service(web::resource("/verify_email").route(web::post().to(user::verify_email))) .service( - web::resource("/v2/verify-email").route(web::post().to(user::verify_email)), + web::resource("/v2/verify_email").route(web::post().to(user::verify_email)), ) .service( web::resource("/verify_email_request") @@ -2057,7 +2057,7 @@ impl User { .route(web::post().to(user_role::accept_invitations_v2)), ) .service( - web::resource("/pre-auth").route( + web::resource("/pre_auth").route( web::post().to(user_role::accept_invitations_pre_auth), ), ), From 15342f7fe7fb081a49fc2d2bb0f7ef5457160442 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:55:44 +0000 Subject: [PATCH 49/51] chore(version): 2024.12.02.1 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 454a5545687..5f748c6924a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.02.1 + +### Bug Fixes + +- **openapi:** Revert Standardise API naming scheme for V2 Dashboard Changes ([#6712](https://github.com/juspay/hyperswitch/pull/6712)) ([`b097d7f`](https://github.com/juspay/hyperswitch/commit/b097d7f5a984b32421494ea033029d01d034fab8)) + +**Full Changelog:** [`2024.12.02.0...2024.12.02.1`](https://github.com/juspay/hyperswitch/compare/2024.12.02.0...2024.12.02.1) + +- - - + ## 2024.12.02.0 ### Features From 982b26a8c2851266b6e8615699479a05ab62e519 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:28:54 +0530 Subject: [PATCH 50/51] ci: update ntid card expiry (#6714) --- .../cypress/e2e/PaymentMethodListUtils/Stripe.js | 2 +- cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js | 4 ++-- cypress-tests/cypress/e2e/PaymentUtils/Checkout.js | 4 ++-- cypress-tests/cypress/e2e/PaymentUtils/Commons.js | 4 ++-- cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js | 8 ++++---- cypress-tests/cypress/e2e/PaymentUtils/Datatrans.js | 2 +- cypress-tests/cypress/e2e/PaymentUtils/Elavon.js | 2 +- cypress-tests/cypress/e2e/PaymentUtils/Fiservemea.js | 2 +- cypress-tests/cypress/e2e/PaymentUtils/Nexixpay.js | 2 +- cypress-tests/cypress/e2e/PaymentUtils/Nmi.js | 4 ++-- cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js | 4 ++-- cypress-tests/cypress/e2e/PaymentUtils/Paybox.js | 4 ++-- cypress-tests/cypress/e2e/PaymentUtils/Paypal.js | 4 ++-- cypress-tests/cypress/e2e/PaymentUtils/Stripe.js | 8 ++++---- cypress-tests/cypress/e2e/PaymentUtils/Trustpay.js | 2 +- cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js | 4 ++-- cypress-tests/cypress/e2e/RoutingUtils/Stripe.js | 4 ++-- cypress-tests/cypress/fixtures/create-confirm-body.json | 2 +- cypress-tests/cypress/fixtures/create-mandate-cit.json | 2 +- cypress-tests/cypress/fixtures/create-ntid-mit.json | 2 +- 20 files changed, 35 insertions(+), 35 deletions(-) diff --git a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js index 9ba67811436..32d138fdc1f 100644 --- a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js +++ b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "morino", card_cvc: "737", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js b/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js index af4dee5d537..6f383367e05 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/BankOfAmerica.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -9,7 +9,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000000000001091", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Checkout.js b/cypress-tests/cypress/e2e/PaymentUtils/Checkout.js index 4653e3e77c2..3307e90c371 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Checkout.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Checkout.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -9,7 +9,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4242424242424242", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js index 0b15113c413..a0eb936152b 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js @@ -44,7 +44,7 @@ function normalise(input) { const successfulNo3DSCardDetails = { card_number: "4111111111111111", card_exp_month: "08", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "999", }; @@ -52,7 +52,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4111111111111111", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "morino", card_cvc: "999", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js b/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js index 72dc8f6479b..c0fcfdd1b53 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -9,7 +9,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000000000001091", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -58,7 +58,7 @@ const payment_method_data_no3ds = { card_isin: "424242", card_extended_bin: null, card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: null, payment_checks: { avs_response: { @@ -82,7 +82,7 @@ const payment_method_data_3ds = { card_isin: "400000", card_extended_bin: null, card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: null, payment_checks: null, authentication_data: null, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Datatrans.js b/cypress-tests/cypress/e2e/PaymentUtils/Datatrans.js index a2285f2c101..13bbf7e7d34 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Datatrans.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Datatrans.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4444090101010103", card_exp_month: "06", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js index 5b985df29da..bb34e805a5a 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4111111111111111", card_exp_month: "06", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Fiservemea.js b/cypress-tests/cypress/e2e/PaymentUtils/Fiservemea.js index 9c5c9e40975..1460b474220 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Fiservemea.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Fiservemea.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "5204740000001002", card_exp_month: "10", - card_exp_year: "24", + card_exp_year: "50", card_holder_name: "Joseph Doe", card_cvc: "002", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Nexixpay.js b/cypress-tests/cypress/e2e/PaymentUtils/Nexixpay.js index 954a4475b7b..7f4927d347f 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Nexixpay.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Nexixpay.js @@ -3,7 +3,7 @@ import { getCustomExchange } from "./Commons"; const successfulNo3DSCardDetails = { card_number: "4012000033330026", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Nmi.js b/cypress-tests/cypress/e2e/PaymentUtils/Nmi.js index f6350ac84bd..2206c516de5 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Nmi.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Nmi.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4000000000002503", card_exp_month: "08", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "999", }; @@ -9,7 +9,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000000000002503", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "morino", card_cvc: "999", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js index e0db22b347d..0b3b2820aa5 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4200000000000000", card_exp_month: "12", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "Max Mustermann", card_cvc: "123", }; @@ -9,7 +9,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000000000001091", card_exp_month: "12", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "Max Mustermann", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js index 70bfa5c3240..a7a4afc07be 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js @@ -9,7 +9,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000000000001091", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -657,7 +657,7 @@ export const connectorDetails = { card: { card_number: "123456", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js b/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js index 4e7f49b1375..715bfae395c 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js @@ -3,7 +3,7 @@ import { getCustomExchange } from "./Commons"; const successfulNo3DSCardDetails = { card_number: "4012000033330026", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -11,7 +11,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4868719460707704", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js b/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js index 984fe63b2fb..5c899e87d9c 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js @@ -3,7 +3,7 @@ import { getCustomExchange } from "./Commons"; const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "morino", card_cvc: "737", }; @@ -11,7 +11,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000002500003155", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "morino", card_cvc: "737", }; @@ -60,7 +60,7 @@ const payment_method_data_3ds = { card_isin: "400000", card_extended_bin: null, card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: null, payment_checks: null, authentication_data: null, @@ -78,7 +78,7 @@ const payment_method_data_no3ds = { card_isin: "424242", card_extended_bin: null, card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: null, payment_checks: { cvc_check: "pass", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Trustpay.js b/cypress-tests/cypress/e2e/PaymentUtils/Trustpay.js index 36d8906fadb..21975b4dea3 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Trustpay.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Trustpay.js @@ -3,7 +3,7 @@ import { getCustomExchange } from "./Commons"; const successfulNo3DSCardDetails = { card_number: "4200000000000000", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js b/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js index 608343f1083..56f3caba8fa 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/WellsFargo.js @@ -1,7 +1,7 @@ const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -9,7 +9,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000000000001091", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "joseph Doe", card_cvc: "123", }; diff --git a/cypress-tests/cypress/e2e/RoutingUtils/Stripe.js b/cypress-tests/cypress/e2e/RoutingUtils/Stripe.js index 9a40c07028e..21e885ac137 100644 --- a/cypress-tests/cypress/e2e/RoutingUtils/Stripe.js +++ b/cypress-tests/cypress/e2e/RoutingUtils/Stripe.js @@ -3,7 +3,7 @@ import {} from "./Commons"; const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "morino", card_cvc: "737", }; @@ -11,7 +11,7 @@ const successfulNo3DSCardDetails = { const successfulThreeDSTestCardDetails = { card_number: "4000002500003155", card_exp_month: "10", - card_exp_year: "25", + card_exp_year: "50", card_holder_name: "morino", card_cvc: "737", }; diff --git a/cypress-tests/cypress/fixtures/create-confirm-body.json b/cypress-tests/cypress/fixtures/create-confirm-body.json index 2a9e8b19ee1..df533116e56 100644 --- a/cypress-tests/cypress/fixtures/create-confirm-body.json +++ b/cypress-tests/cypress/fixtures/create-confirm-body.json @@ -27,7 +27,7 @@ "card": { "card_number": "4242424242424242", "card_exp_month": "01", - "card_exp_year": "24", + "card_exp_year": "50", "card_holder_name": "joseph Doe", "card_cvc": "123" } diff --git a/cypress-tests/cypress/fixtures/create-mandate-cit.json b/cypress-tests/cypress/fixtures/create-mandate-cit.json index d33cb8b91c7..d23c1f80543 100644 --- a/cypress-tests/cypress/fixtures/create-mandate-cit.json +++ b/cypress-tests/cypress/fixtures/create-mandate-cit.json @@ -18,7 +18,7 @@ "card": { "card_number": "4242424242424242", "card_exp_month": "10", - "card_exp_year": "25", + "card_exp_year": "50", "card_holder_name": "joseph Doe", "card_cvc": "123" } diff --git a/cypress-tests/cypress/fixtures/create-ntid-mit.json b/cypress-tests/cypress/fixtures/create-ntid-mit.json index 93b7e95e2a0..61e86828338 100644 --- a/cypress-tests/cypress/fixtures/create-ntid-mit.json +++ b/cypress-tests/cypress/fixtures/create-ntid-mit.json @@ -10,7 +10,7 @@ "data": { "card_number": "4242424242424242", "card_exp_month": "11", - "card_exp_year": "2024", + "card_exp_year": "2050", "card_holder_name": "joseph Doe", "network_transaction_id": "MCC5ZRGMI0925" } From 797a0db7733c5b387564fb1bbc106d054c8dffa6 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:29:28 +0530 Subject: [PATCH 51/51] feat(payment_methods_v2): implement a barebones version of list customer payment methods v2 (#6649) --- api-reference-v2/openapi_spec.json | 8 +- api-reference/openapi_spec.json | 4 +- crates/api_models/src/payment_methods.rs | 14 +- crates/common_utils/src/id_type/global_id.rs | 4 +- .../src/id_type/global_id/payment.rs | 4 +- .../src/id_type/global_id/payment_methods.rs | 5 +- .../src/id_type/global_id/refunds.rs | 2 +- .../src/payment_methods.rs | 7 +- crates/router/src/core/payment_methods.rs | 179 ++++++++++-------- .../router/src/core/payment_methods/cards.rs | 30 +-- crates/router/src/core/payments.rs | 2 +- crates/router/src/lib.rs | 9 +- crates/router/src/routes/app.rs | 8 +- crates/router/src/services/authentication.rs | 48 ----- .../src/types/storage/payment_method.rs | 36 ++++ 15 files changed, 173 insertions(+), 187 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 2b66d1755ff..e3f54813a43 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -7153,6 +7153,7 @@ "customer_id", "payment_method_type", "recurring_enabled", + "created", "requires_cvv", "is_default" ], @@ -7210,9 +7211,8 @@ "created": { "type": "string", "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", - "example": "2023-01-18T11:04:09.922Z", - "nullable": true + "description": "A timestamp (ISO 8601 code) that determines when the payment method was created", + "example": "2023-01-18T11:04:09.922Z" }, "surcharge_details": { "allOf": [ @@ -13542,7 +13542,7 @@ "created": { "type": "string", "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was created", "example": "2023-01-18T11:04:09.922Z", "nullable": true }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d2133a3e68e..25ed2648cd4 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9739,7 +9739,7 @@ "created": { "type": "string", "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was created", "example": "2023-01-18T11:04:09.922Z", "nullable": true }, @@ -16326,7 +16326,7 @@ "created": { "type": "string", "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was created", "example": "2023-01-18T11:04:09.922Z", "nullable": true }, diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 2c2aa4861c5..283a0d662d7 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -755,7 +755,7 @@ pub struct PaymentMethodResponse { #[schema(value_type = Option, example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, - /// A timestamp (ISO 8601 code) that determines when the customer was created + /// A timestamp (ISO 8601 code) that determines when the payment method was created #[schema(value_type = Option, example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created: Option, @@ -801,7 +801,7 @@ pub struct PaymentMethodResponse { #[schema(example = true)] pub recurring_enabled: bool, - /// A timestamp (ISO 8601 code) that determines when the customer was created + /// A timestamp (ISO 8601 code) that determines when the payment method was created #[schema(value_type = Option, example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created: Option, @@ -1802,10 +1802,10 @@ pub struct CustomerPaymentMethod { #[schema(example = json!({"mask": "0000"}))] pub bank: Option, - /// A timestamp (ISO 8601 code) that determines when the customer was created - #[schema(value_type = Option,example = "2023-01-18T11:04:09.922Z")] - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub created: Option, + /// A timestamp (ISO 8601 code) that determines when the payment method was created + #[schema(value_type = PrimitiveDateTime, example = "2023-01-18T11:04:09.922Z")] + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created: time::PrimitiveDateTime, /// Surcharge details for this saved card pub surcharge_details: Option, @@ -1890,7 +1890,7 @@ pub struct CustomerPaymentMethod { #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, - /// A timestamp (ISO 8601 code) that determines when the customer was created + /// A timestamp (ISO 8601 code) that determines when the payment method was created #[schema(value_type = Option,example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created: Option, diff --git a/crates/common_utils/src/id_type/global_id.rs b/crates/common_utils/src/id_type/global_id.rs index 5490dcda7bd..a54df758587 100644 --- a/crates/common_utils/src/id_type/global_id.rs +++ b/crates/common_utils/src/id_type/global_id.rs @@ -120,7 +120,7 @@ pub(crate) enum GlobalIdError { impl GlobalId { /// Create a new global id from entity and cell information /// The entity prefix is used to identify the entity, `cus` for customers, `pay`` for payments etc. - pub fn generate(cell_id: CellId, entity: GlobalEntity) -> Self { + pub fn generate(cell_id: &CellId, entity: GlobalEntity) -> Self { let prefix = format!("{}_{}", cell_id.get_string_repr(), entity.prefix()); let id = generate_time_ordered_id(&prefix); let alphanumeric_id = AlphaNumericId::new_unchecked(id); @@ -201,7 +201,7 @@ mod global_id_tests { let cell_id_string = "12345"; let entity = GlobalEntity::Customer; let cell_id = CellId::from_str(cell_id_string).unwrap(); - let global_id = GlobalId::generate(cell_id, entity); + let global_id = GlobalId::generate(&cell_id, entity); /// Generate a regex for globalid /// Eg - 12abc_cus_abcdefghijklmnopqrstuvwxyz1234567890 diff --git a/crates/common_utils/src/id_type/global_id/payment.rs b/crates/common_utils/src/id_type/global_id/payment.rs index 934d710604c..5a2da3998bb 100644 --- a/crates/common_utils/src/id_type/global_id/payment.rs +++ b/crates/common_utils/src/id_type/global_id/payment.rs @@ -20,7 +20,7 @@ impl GlobalPaymentId { } /// Generate a new GlobalPaymentId from a cell id - pub fn generate(cell_id: crate::id_type::CellId) -> Self { + pub fn generate(cell_id: &crate::id_type::CellId) -> Self { let global_id = super::GlobalId::generate(cell_id, super::GlobalEntity::Payment); Self(global_id) } @@ -57,7 +57,7 @@ crate::impl_to_sql_from_sql_global_id_type!(GlobalAttemptId); impl GlobalAttemptId { /// Generate a new GlobalAttemptId from a cell id pub fn generate(cell_id: &super::CellId) -> Self { - let global_id = super::GlobalId::generate(cell_id.clone(), super::GlobalEntity::Attempt); + let global_id = super::GlobalId::generate(cell_id, super::GlobalEntity::Attempt); Self(global_id) } diff --git a/crates/common_utils/src/id_type/global_id/payment_methods.rs b/crates/common_utils/src/id_type/global_id/payment_methods.rs index f6f394242cc..40bd6ec0df4 100644 --- a/crates/common_utils/src/id_type/global_id/payment_methods.rs +++ b/crates/common_utils/src/id_type/global_id/payment_methods.rs @@ -28,10 +28,7 @@ pub enum GlobalPaymentMethodIdError { impl GlobalPaymentMethodId { /// Create a new GlobalPaymentMethodId from cell id information - pub fn generate(cell_id: &str) -> error_stack::Result { - let cell_id = CellId::from_str(cell_id) - .change_context(GlobalPaymentMethodIdError::ConstructionError) - .attach_printable("Failed to construct CellId from str")?; + pub fn generate(cell_id: &CellId) -> error_stack::Result { let global_id = GlobalId::generate(cell_id, GlobalEntity::PaymentMethod); Ok(Self(global_id)) } diff --git a/crates/common_utils/src/id_type/global_id/refunds.rs b/crates/common_utils/src/id_type/global_id/refunds.rs index 64e47516114..0aac9bf5808 100644 --- a/crates/common_utils/src/id_type/global_id/refunds.rs +++ b/crates/common_utils/src/id_type/global_id/refunds.rs @@ -26,7 +26,7 @@ impl GlobalRefundId { } /// Generate a new GlobalRefundId from a cell id - pub fn generate(cell_id: crate::id_type::CellId) -> Self { + pub fn generate(cell_id: &crate::id_type::CellId) -> Self { let global_id = super::GlobalId::generate(cell_id, super::GlobalEntity::Refund); Self(global_id) } diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index 083d6e47501..4e7d727839b 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -12,7 +12,7 @@ use masking::{PeekInterface, Secret}; use time::PrimitiveDateTime; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use crate::type_encryption::EncryptedJsonType; +use crate::type_encryption::OptionalEncryptableJsonType; use crate::type_encryption::{crypto_operation, AsyncLift, CryptoOperation}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -80,9 +80,8 @@ pub struct PaymentMethod { pub last_modified: PrimitiveDateTime, pub payment_method_type: Option, pub payment_method_subtype: Option, - pub payment_method_data: Option< - Encryptable>>, - >, + pub payment_method_data: + OptionalEncryptableJsonType, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, pub connector_mandate_details: Option, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 8b8b3f33ed8..599d696ec06 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -865,9 +865,10 @@ pub async fn create_payment_method( .attach_printable("Unable to encrypt Payment method billing address")?; // create pm - let payment_method_id = id_type::GlobalPaymentMethodId::generate("random_cell_id") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to generate GlobalPaymentMethodId")?; + let payment_method_id = + id_type::GlobalPaymentMethodId::generate(&state.conf.cell_information.id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to generate GlobalPaymentMethodId")?; let payment_method = create_payment_method_for_intent( state, @@ -978,9 +979,10 @@ pub async fn payment_method_intent_create( // create pm entry - let payment_method_id = id_type::GlobalPaymentMethodId::generate("random_cell_id") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to generate GlobalPaymentMethodId")?; + let payment_method_id = + id_type::GlobalPaymentMethodId::generate(&state.conf.cell_information.id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to generate GlobalPaymentMethodId")?; let payment_method = create_payment_method_for_intent( state, @@ -1324,69 +1326,82 @@ pub async fn vault_payment_method( feature = "customer_v2" ))] async fn get_pm_list_context( - state: &SessionState, - payment_method: &enums::PaymentMethod, - _key_store: &domain::MerchantKeyStore, - pm: &domain::PaymentMethod, - _parent_payment_method_token: Option, + payment_method_type: enums::PaymentMethod, + payment_method: &domain::PaymentMethod, is_payment_associated: bool, ) -> Result, error_stack::Report> { - let payment_method_retrieval_context = match payment_method { - enums::PaymentMethod::Card => { - let card_details = cards::get_card_details_with_locker_fallback(pm, state).await?; - - card_details.as_ref().map(|card| PaymentMethodListContext { - card_details: Some(card.clone()), - #[cfg(feature = "payouts")] - bank_transfer_details: None, - hyperswitch_token_data: is_payment_associated.then_some( + let payment_method_data = payment_method + .payment_method_data + .clone() + .map(|payment_method_data| payment_method_data.into_inner().expose().into_inner()); + + let payment_method_retrieval_context = match payment_method_data { + Some(payment_methods::PaymentMethodsData::Card(card)) => { + Some(PaymentMethodListContext::Card { + card_details: api::CardDetailFromLocker::from(card), + token_data: is_payment_associated.then_some( storage::PaymentTokenData::permanent_card( - Some(pm.get_id().clone()), - pm.locker_id + Some(payment_method.get_id().clone()), + payment_method + .locker_id .as_ref() - .map(|id| id.get_string_repr().clone()) - .or(Some(pm.get_id().get_string_repr().to_owned())), - pm.locker_id + .map(|id| id.get_string_repr().to_owned()) + .or_else(|| Some(payment_method.get_id().get_string_repr().to_owned())), + payment_method + .locker_id .as_ref() - .map(|id| id.get_string_repr().clone()) - .unwrap_or(pm.get_id().get_string_repr().to_owned()), + .map(|id| id.get_string_repr().to_owned()) + .unwrap_or_else(|| { + payment_method.get_id().get_string_repr().to_owned() + }), ), ), }) } + Some(payment_methods::PaymentMethodsData::BankDetails(bank_details)) => { + let get_bank_account_token_data = + || -> errors::CustomResult { + let connector_details = bank_details + .connector_details + .first() + .cloned() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to obtain bank account connector details")?; + + let payment_method_subtype = payment_method + .get_payment_method_subtype() + .get_required_value("payment_method_subtype") + .attach_printable("PaymentMethodType not found")?; + + Ok(payment_methods::BankAccountTokenData { + payment_method_type: payment_method_subtype, + payment_method: payment_method_type, + connector_details, + }) + }; - enums::PaymentMethod::BankDebit => { // Retrieve the pm_auth connector details so that it can be tokenized - let bank_account_token_data = cards::get_bank_account_connector_details(pm) - .await - .unwrap_or_else(|err| { - logger::error!(error=?err); - None - }); - + let bank_account_token_data = get_bank_account_token_data() + .inspect_err(|error| logger::error!(?error)) + .ok(); bank_account_token_data.map(|data| { let token_data = storage::PaymentTokenData::AuthBankDebit(data); - PaymentMethodListContext { - card_details: None, - #[cfg(feature = "payouts")] - bank_transfer_details: None, - hyperswitch_token_data: is_payment_associated.then_some(token_data), + PaymentMethodListContext::Bank { + token_data: is_payment_associated.then_some(token_data), } }) } - - _ => Some(PaymentMethodListContext { - card_details: None, - #[cfg(feature = "payouts")] - bank_transfer_details: None, - hyperswitch_token_data: is_payment_associated.then_some( - storage::PaymentTokenData::temporary_generic(generate_id( - consts::ID_LENGTH, - "token", - )), - ), - }), + Some(payment_methods::PaymentMethodsData::WalletDetails(_)) | None => { + Some(PaymentMethodListContext::TemporaryToken { + token_data: is_payment_associated.then_some( + storage::PaymentTokenData::temporary_generic(generate_id( + consts::ID_LENGTH, + "token", + )), + ), + }) + } }; Ok(payment_method_retrieval_context) @@ -1471,9 +1486,9 @@ pub async fn list_customer_payment_method( let key_manager_state = &(state).into(); let customer = db - .find_customer_by_merchant_reference_id_merchant_id( + .find_customer_by_global_id( key_manager_state, - customer_id, + customer_id.get_string_repr(), merchant_account.get_id(), &key_store, merchant_account.storage_scheme, @@ -1509,25 +1524,23 @@ pub async fn list_customer_payment_method( .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let mut filtered_saved_payment_methods_ctx = Vec::new(); - for pm in saved_payment_methods.into_iter() { - let payment_method = pm + for payment_method in saved_payment_methods.into_iter() { + let payment_method_type = payment_method .get_payment_method_type() .get_required_value("payment_method")?; let parent_payment_method_token = is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token")); - let pm_list_context = get_pm_list_context( - state, - &payment_method, - &key_store, - &pm, - parent_payment_method_token.clone(), - is_payment_associated, - ) - .await?; + let pm_list_context = + get_pm_list_context(payment_method_type, &payment_method, is_payment_associated) + .await?; if let Some(ctx) = pm_list_context { - filtered_saved_payment_methods_ctx.push((ctx, parent_payment_method_token, pm)); + filtered_saved_payment_methods_ctx.push(( + ctx, + parent_payment_method_token, + payment_method, + )); } } @@ -1576,6 +1589,8 @@ pub async fn list_customer_payment_method( is_guest_customer: is_payment_associated.then_some(false), //to return this key only when the request is tied to a payment intent }; + /* + TODO: Implement surcharge for v2 if is_payment_associated { Box::pin(cards::perform_surcharge_ops( payments_info.as_ref().map(|pi| pi.payment_intent.clone()), @@ -1587,6 +1602,7 @@ pub async fn list_customer_payment_method( )) .await?; } + */ Ok(services::ApplicationResponse::Json(response)) } @@ -1661,15 +1677,20 @@ async fn generate_saved_pm_response( requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()) }; - let pmd = if let Some(card) = pm_list_context.card_details.as_ref() { - Some(api::PaymentMethodListData::Card(card.clone())) - } else if cfg!(feature = "payouts") { - pm_list_context - .bank_transfer_details - .clone() - .map(api::PaymentMethodListData::Bank) - } else { - None + let pmd = match &pm_list_context { + PaymentMethodListContext::Card { card_details, .. } => { + Some(api::PaymentMethodListData::Card(card_details.clone())) + } + #[cfg(feature = "payouts")] + PaymentMethodListContext::BankTransfer { + bank_transfer_details, + .. + } => Some(api::PaymentMethodListData::Bank( + bank_transfer_details.clone(), + )), + PaymentMethodListContext::Bank { .. } | PaymentMethodListContext::TemporaryToken { .. } => { + None + } }; let pma = api::CustomerPaymentMethod { @@ -1680,7 +1701,7 @@ async fn generate_saved_pm_response( payment_method_subtype: pm.get_payment_method_subtype(), payment_method_data: pmd, recurring_enabled: mca_enabled, - created: Some(pm.created_at), + created: pm.created_at, bank: bank_details, surcharge_details: None, requires_cvv: requires_cvv @@ -1952,8 +1973,8 @@ impl pm_types::SavedPMLPaymentsInfo { let token = parent_payment_method_token .as_ref() .get_required_value("parent_payment_method_token")?; - let hyperswitch_token_data = pm_list_context - .hyperswitch_token_data + let token_data = pm_list_context + .get_token_data() .get_required_value("PaymentTokenData")?; let intent_fulfillment_time = self @@ -1962,7 +1983,7 @@ impl pm_types::SavedPMLPaymentsInfo { .unwrap_or(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME); pm_routes::ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method_type)) - .insert(intent_fulfillment_time, hyperswitch_token_data, state) + .insert(intent_fulfillment_time, token_data, state) .await?; Ok(()) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 955cc37b0d9..9cfe6e6ec73 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -5328,14 +5328,6 @@ pub async fn get_card_details_with_locker_fallback( }) } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub async fn get_card_details_with_locker_fallback( - pm: &domain::PaymentMethod, - state: &routes::SessionState, -) -> errors::RouterResult> { - todo!() -} - #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") @@ -5365,14 +5357,6 @@ pub async fn get_card_details_without_locker_fallback( }) } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub async fn get_card_details_without_locker_fallback( - pm: &domain::PaymentMethod, - state: &routes::SessionState, -) -> errors::RouterResult { - todo!() -} - #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") @@ -5460,13 +5444,13 @@ pub async fn get_masked_bank_details( } } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub async fn get_bank_account_connector_details( pm: &domain::PaymentMethod, ) -> errors::RouterResult> { - #[cfg(all( - any(feature = "v2", feature = "v1"), - not(feature = "payment_methods_v2") - ))] let payment_method_data = pm .payment_method_data .clone() @@ -5481,12 +5465,6 @@ pub async fn get_bank_account_connector_details( ) .transpose()?; - #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] - let payment_method_data = pm - .payment_method_data - .clone() - .map(|x| x.into_inner().expose().into_inner()); - match payment_method_data { Some(pmd) => match pmd { PaymentMethodsData::Card(_) => Err(errors::ApiErrorResponse::UnprocessableEntity { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5eb97e6eebe..fd61fcacf5a 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1019,7 +1019,7 @@ where .to_validate_request()? .validate_request(&req, &merchant_account)?; - let payment_id = id_type::GlobalPaymentId::generate(state.conf.cell_information.id.clone()); + let payment_id = id_type::GlobalPaymentId::generate(&state.conf.cell_information.id.clone()); tracing::Span::current().record("global_payment_id", payment_id.get_string_repr()); diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 829216db1dd..839dd472423 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -132,7 +132,7 @@ pub fn mk_app( .service(routes::Forex::server(state.clone())); } - server_app = server_app.service(routes::Profile::server(state.clone())) + server_app = server_app.service(routes::Profile::server(state.clone())); } server_app = server_app .service(routes::Payments::server(state.clone())) @@ -141,6 +141,11 @@ pub fn mk_app( .service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::Webhooks::server(state.clone())); + #[cfg(feature = "oltp")] + { + server_app = server_app.service(routes::PaymentMethods::server(state.clone())); + } + #[cfg(feature = "v1")] { server_app = server_app @@ -157,8 +162,6 @@ pub fn mk_app( { server_app = server_app .service(routes::EphemeralKey::server(state.clone())) - .service(routes::Webhooks::server(state.clone())) - .service(routes::PaymentMethods::server(state.clone())) .service(routes::Poll::server(state.clone())) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 7a8571479f4..1964c1cbfa5 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -954,6 +954,10 @@ pub struct Customers; impl Customers { pub fn server(state: AppState) -> Scope { let mut route = web::scope("/v2/customers").app_data(web::Data::new(state)); + #[cfg(all(feature = "olap", feature = "v2", feature = "customer_v2"))] + { + route = route.service(web::resource("/list").route(web::get().to(customers_list))) + } #[cfg(all(feature = "oltp", feature = "v2", feature = "customer_v2"))] { route = route @@ -965,10 +969,6 @@ impl Customers { .route(web::delete().to(customers_delete)), ) } - #[cfg(all(feature = "olap", feature = "v2", feature = "customer_v2"))] - { - route = route.service(web::resource("/list").route(web::get().to(customers_list))) - } #[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2"))] { route = route.service( diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 724539ad08a..1967eafd180 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1323,7 +1323,6 @@ where #[derive(Debug)] pub struct EphemeralKeyAuth; -// #[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for EphemeralKeyAuth where @@ -2987,43 +2986,6 @@ pub fn get_auth_type_and_flow( Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) } -#[cfg(feature = "v1")] -pub fn check_client_secret_and_get_auth( - headers: &HeaderMap, - payload: &impl ClientSecretFetch, -) -> RouterResult<( - Box>, - api::AuthFlow, -)> -where - T: SessionStateInfo + Sync + Send, - ApiKeyAuth: AuthenticateAndFetch, - PublishableKeyAuth: AuthenticateAndFetch, -{ - let api_key = get_api_key(headers)?; - if api_key.starts_with("pk_") { - payload - .get_client_secret() - .check_value_present("client_secret") - .map_err(|_| errors::ApiErrorResponse::MissingRequiredField { - field_name: "client_secret", - })?; - return Ok(( - Box::new(HeaderAuth(PublishableKeyAuth)), - api::AuthFlow::Client, - )); - } - - if payload.get_client_secret().is_some() { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "client_secret is not a valid parameter".to_owned(), - } - .into()); - } - Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) -} - -#[cfg(feature = "v2")] pub fn check_client_secret_and_get_auth( headers: &HeaderMap, payload: &impl ClientSecretFetch, @@ -3056,11 +3018,9 @@ where } .into()); } - Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) } -#[cfg(feature = "v1")] pub async fn get_ephemeral_or_other_auth( headers: &HeaderMap, is_merchant_flow: bool, @@ -3093,7 +3053,6 @@ where } } -#[cfg(feature = "v1")] pub fn is_ephemeral_auth( headers: &HeaderMap, ) -> RouterResult>> { @@ -3106,13 +3065,6 @@ pub fn is_ephemeral_auth( } } -#[cfg(feature = "v2")] -pub fn is_ephemeral_auth( - headers: &HeaderMap, -) -> RouterResult>> { - todo!() -} - pub fn is_jwt_auth(headers: &HeaderMap) -> bool { headers.get(headers::AUTHORIZATION).is_some() || get_cookie_from_header(headers) diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 3214f911f56..bc5f6651b6b 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -105,6 +105,10 @@ impl PaymentTokenData { } } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentMethodListContext { pub card_details: Option, @@ -113,6 +117,38 @@ pub struct PaymentMethodListContext { pub bank_transfer_details: Option, } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum PaymentMethodListContext { + Card { + card_details: api::CardDetailFromLocker, + token_data: Option, + }, + Bank { + token_data: Option, + }, + #[cfg(feature = "payouts")] + BankTransfer { + bank_transfer_details: api::BankPayout, + token_data: Option, + }, + TemporaryToken { + token_data: Option, + }, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl PaymentMethodListContext { + pub(crate) fn get_token_data(&self) -> Option { + match self { + Self::Card { token_data, .. } + | Self::Bank { token_data } + | Self::BankTransfer { token_data, .. } + | Self::TemporaryToken { token_data } => token_data.clone(), + } + } +} + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct PaymentMethodStatusTrackingData { pub payment_method_id: String,