From 9a201ae698c2cf52e617660f82d5bf1df2e797ae Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:48:42 +0530 Subject: [PATCH 01/13] fix(router): add rust locker url in proxy_bypass_urls (#2902) --- crates/router/src/services/api/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index 8eb6ab72f988..cc7353dcda6b 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -110,11 +110,15 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); + let locker_host_rs = locker.host_rs.to_owned(); let basilisk_host = locker.basilisk_host.to_owned(); vec![ format!("{locker_host}/cards/add"), format!("{locker_host}/cards/retrieve"), format!("{locker_host}/cards/delete"), + format!("{locker_host_rs}/cards/add"), + format!("{locker_host_rs}/cards/retrieve"), + format!("{locker_host_rs}/cards/delete"), format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), From 1d48a83c485c27cced7ce1441060333bbb54dbc7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 12:21:10 +0000 Subject: [PATCH 02/13] chore(version): v1.83.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4270442611a8..d5d04d15669e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.83.0 (2023-11-17) + +### Features + +- **events:** Add incoming webhook payload to api events logger ([#2852](https://github.com/juspay/hyperswitch/pull/2852)) ([`aea390a`](https://github.com/juspay/hyperswitch/commit/aea390a6a1c331f8e0dbea4f41218e43f7323508)) +- **router:** Custom payment link config for payment create ([#2741](https://github.com/juspay/hyperswitch/pull/2741)) ([`c39beb2`](https://github.com/juspay/hyperswitch/commit/c39beb2501e63bbf7fd41bbc947280d7ff5a71dc)) + +### Bug Fixes + +- **router:** Add rust locker url in proxy_bypass_urls ([#2902](https://github.com/juspay/hyperswitch/pull/2902)) ([`9a201ae`](https://github.com/juspay/hyperswitch/commit/9a201ae698c2cf52e617660f82d5bf1df2e797ae)) + +### Documentation + +- **README:** Replace cloudformation deployment template with latest s3 url. ([#2891](https://github.com/juspay/hyperswitch/pull/2891)) ([`375108b`](https://github.com/juspay/hyperswitch/commit/375108b6df50e041fc9dbeb35a6a6b46b146037a)) + +**Full Changelog:** [`v1.82.0...v1.83.0`](https://github.com/juspay/hyperswitch/compare/v1.82.0...v1.83.0) + +- - - + + ## 1.82.0 (2023-11-17) ### Features From 606daa9367cac8c2ea926313019deab2f938b591 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:19:41 +0530 Subject: [PATCH 03/13] fix(router): add choice to use the appropriate key for jws verification (#2917) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../router/src/core/payment_methods/cards.rs | 39 +++++++++++-------- .../src/core/payment_methods/transformers.rs | 17 +++++++- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 4ab7d334f883..80daf66a6926 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -440,10 +440,11 @@ pub async fn get_payment_method_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchPaymentMethodFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, locker_choice) + .await + .change_context(errors::VaultError::FetchPaymentMethodFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; @@ -490,10 +491,11 @@ pub async fn call_to_locker_hs<'a>( .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting decrypted response payload")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting decrypted response payload")?; let stored_card_resp: payment_methods::StoreCardResp = decrypted_payload .parse_struct("StoreCardResp") .change_context(errors::VaultError::ResponseDeserializationFailed)?; @@ -557,10 +559,11 @@ pub async fn get_card_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchCardFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::FetchCardFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchCardFailed)?; @@ -609,10 +612,14 @@ pub async fn delete_card_from_hs_locker<'a>( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while executing call_connector_api for delete card"); let jwe_body: services::JweBody = response.get_response_inner("JweBody")?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting decrypted response payload for delete card")?; + let decrypted_payload = payment_methods::get_decrypted_response_payload( + jwekey, + jwe_body, + Some(api_enums::LockerChoice::Basilisk), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting decrypted response payload for delete card")?; let delete_card_resp: payment_methods::DeleteCardResp = decrypted_payload .parse_struct("DeleteCardResp") .change_context(errors::ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 45182411c28c..3b4d057e6025 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -189,14 +189,27 @@ pub async fn get_decrypted_response_payload( #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, jwe_body: encryption::JweBody, + locker_choice: Option, ) -> CustomResult { + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); + #[cfg(feature = "kms")] - let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => { + jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes() + } + }; + #[cfg(feature = "kms")] let private_key = jwekey.jwekey.peek().vault_private_key.as_bytes(); #[cfg(not(feature = "kms"))] - let public_key = jwekey.vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(), + }; + #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.as_bytes(); From 0a88336b443e240d16837c43f6edd59455ad4cb6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:51:54 +0000 Subject: [PATCH 04/13] chore(version): v1.83.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d04d15669e..021a0326f025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.83.1 (2023-11-17) + +### Bug Fixes + +- **router:** Add choice to use the appropriate key for jws verification ([#2917](https://github.com/juspay/hyperswitch/pull/2917)) ([`606daa9`](https://github.com/juspay/hyperswitch/commit/606daa9367cac8c2ea926313019deab2f938b591)) + +**Full Changelog:** [`v1.83.0...v1.83.1`](https://github.com/juspay/hyperswitch/compare/v1.83.0...v1.83.1) + +- - - + + ## 1.83.0 (2023-11-17) ### Features From bdcc138e8d84577fc99f9a9aef3484b66f98209a Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:38:52 +0530 Subject: [PATCH 05/13] feat(connector): [BANKOFAMERICA] PSYNC Bugfix (#2897) --- .../connector/bankofamerica/transformers.rs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 20b2af48b168..a6fa8652b27d 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -273,7 +273,8 @@ pub enum BankofamericaPaymentStatus { impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { fn foreign_from((status, auto_capture): (BankofamericaPaymentStatus, bool)) -> Self { match status { - BankofamericaPaymentStatus::Authorized => { + BankofamericaPaymentStatus::Authorized + | BankofamericaPaymentStatus::AuthorizedPendingReview => { if auto_capture { // Because BankOfAmerica will return Payment Status as Authorized even in AutoCapture Payment Self::Pending @@ -281,7 +282,6 @@ impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { Self::Authorized } } - BankofamericaPaymentStatus::AuthorizedPendingReview => Self::Authorized, BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { Self::Charged } @@ -321,7 +321,7 @@ pub struct BankOfAmericaErrorInformationResponse { #[derive(Debug, Deserialize)] pub struct BankOfAmericaErrorInformation { reason: Option, - message: String, + message: Option, } impl @@ -369,7 +369,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, @@ -422,7 +425,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, @@ -475,7 +481,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, From 94897d841e25d0be8debdfe1ec674f28848e2ad4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:57:32 +0000 Subject: [PATCH 06/13] chore(version): v1.84.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 021a0326f025..141bfd40ac5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.84.0 (2023-11-17) + +### Features + +- **connector:** [BANKOFAMERICA] PSYNC Bugfix ([#2897](https://github.com/juspay/hyperswitch/pull/2897)) ([`bdcc138`](https://github.com/juspay/hyperswitch/commit/bdcc138e8d84577fc99f9a9aef3484b66f98209a)) + +**Full Changelog:** [`v1.83.1...v1.84.0`](https://github.com/juspay/hyperswitch/compare/v1.83.1...v1.84.0) + +- - - + + ## 1.83.1 (2023-11-17) ### Bug Fixes From 25cef386b8876b43893f20b93cd68ece6e68412d Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:54:55 +0530 Subject: [PATCH 07/13] feat(mca): Add new `auth_type` and a status field for mca (#2883) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/admin.rs | 9 +++ crates/common_enums/src/enums.rs | 22 +++++++ crates/diesel_models/src/enums.rs | 7 ++- .../src/merchant_connector_account.rs | 4 ++ crates/diesel_models/src/schema.rs | 1 + crates/kgraph_utils/benches/evaluation.rs | 1 + crates/kgraph_utils/src/mca.rs | 1 + .../src/connector/square/transformers.rs | 1 + crates/router/src/core/admin.rs | 62 ++++++++++++++++++- crates/router/src/core/verification/utils.rs | 1 + .../src/db/merchant_connector_account.rs | 2 + crates/router/src/openapi.rs | 1 + crates/router/src/types.rs | 1 + .../domain/merchant_connector_account.rs | 7 +++ crates/router/src/types/transformers.rs | 1 + .../down.sql | 3 + .../up.sql | 11 ++++ openapi/openapi_spec.json | 25 +++++++- 18 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 migrations/2023-11-12-131143_connector-status-column/down.sql create mode 100644 migrations/2023-11-12-131143_connector-status-column/up.sql diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 6b9928734cef..efde4a048323 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -609,6 +609,9 @@ pub struct MerchantConnectorCreate { pub profile_id: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -714,6 +717,9 @@ pub struct MerchantConnectorResponse { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: api_enums::ConnectorStatus, } /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." @@ -788,6 +794,9 @@ pub struct MerchantConnectorUpdate { pub connector_webhook_details: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, } ///Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 8b1437fa8926..cf3c398f8f48 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1857,3 +1857,25 @@ pub enum ApplePayFlow { Simplified, Manual, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + strum::Display, + strum::EnumString, + serde::Deserialize, + serde::Serialize, + ToSchema, + Default, +)] +#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ConnectorStatus { + #[default] + Inactive, + Active, +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index ec021f0f51a5..817fee633190 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -3,9 +3,10 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, - DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, - DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, + DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, + DbEventObjectType as EventObjectType, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFraudCheckType as FraudCheckType, DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbMandateType as MandateType, diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index a4faa45ce4bc..e45ef0026261 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -42,6 +42,7 @@ pub struct MerchantConnectorAccount { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -70,6 +71,7 @@ pub struct MerchantConnectorAccountNew { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -93,6 +95,7 @@ pub struct MerchantConnectorAccountUpdateInternal { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: Option, } impl MerchantConnectorAccountUpdateInternal { @@ -115,6 +118,7 @@ impl MerchantConnectorAccountUpdateInternal { frm_config: self.frm_config, modified_at: self.modified_at.unwrap_or(source.modified_at), pm_auth_config: self.pm_auth_config, + status: self.status.unwrap_or(source.status), ..source } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e9db5714bed8..190a123185e4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -492,6 +492,7 @@ diesel::table! { profile_id -> Nullable, applepay_verified_domains -> Nullable>>, pm_auth_config -> Nullable, + status -> ConnectorStatus, } } diff --git a/crates/kgraph_utils/benches/evaluation.rs b/crates/kgraph_utils/benches/evaluation.rs index ecea12203f8a..6105dc85d7e6 100644 --- a/crates/kgraph_utils/benches/evaluation.rs +++ b/crates/kgraph_utils/benches/evaluation.rs @@ -65,6 +65,7 @@ fn build_test_data<'a>(total_enabled: usize, total_pm_types: usize) -> graph::Kn profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; kgraph_utils::mca::make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index 34babd7a02bd..deea51bd8808 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -410,6 +410,7 @@ mod tests { profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 54a7c461dbfc..dfb49e8e6775 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -334,6 +334,7 @@ impl TryFrom<&types::ConnectorAuthType> for SquareAuthType { | types::ConnectorAuthType::SignatureKey { .. } | types::ConnectorAuthType::MultiAuthKey { .. } | types::ConnectorAuthType::CurrencyAuthKey { .. } + | types::ConnectorAuthType::TemporaryAuth { .. } | types::ConnectorAuthType::NoKey { .. } => { Err(errors::ConnectorError::FailedToObtainAuthType.into()) } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 39b4749535b7..c921a9164cb0 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -868,6 +868,15 @@ pub async fn create_payment_connector( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error updating the merchant account when creating payment connector")?; + let (connector_status, disabled) = validate_status_and_disabled( + req.status, + req.disabled, + auth, + // The validate_status_and_disabled function will use this value only + // when the status can be active. So we are passing this as fallback. + api_enums::ConnectorStatus::Active, + )?; + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -886,7 +895,7 @@ pub async fn create_payment_connector( .attach_printable("Unable to encrypt connector account details")?, payment_methods_enabled, test_mode: req.test_mode, - disabled: req.disabled, + disabled, metadata: req.metadata, frm_configs, connector_label: Some(connector_label), @@ -911,6 +920,7 @@ pub async fn create_payment_connector( profile_id: Some(profile_id.clone()), applepay_verified_domains: None, pm_auth_config: req.pm_auth_config.clone(), + status: connector_status, }; let mut default_routing_config = @@ -1083,6 +1093,19 @@ pub async fn update_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); + let auth: types::ConnectorAuthType = req + .connector_account_details + .clone() + .unwrap_or(mca.connector_account_details.clone().into_inner()) + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "connector_account_details".to_string(), + expected_format: "auth_type and api_key".to_string(), + })?; + + let (connector_status, disabled) = + validate_status_and_disabled(req.status, req.disabled, auth, mca.status)?; + let payment_connector = storage::MerchantConnectorAccountUpdate::Update { merchant_id: None, connector_type: Some(req.connector_type), @@ -1098,7 +1121,7 @@ pub async fn update_payment_connector( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while encrypting data")?, test_mode: req.test_mode, - disabled: req.disabled, + disabled, payment_methods_enabled, metadata: req.metadata, frm_configs, @@ -1115,6 +1138,7 @@ pub async fn update_payment_connector( }, applepay_verified_domains: None, pm_auth_config: req.pm_auth_config, + status: Some(connector_status), }; let updated_mca = db @@ -1722,3 +1746,37 @@ pub async fn validate_dummy_connector_enabled( Ok(()) } } + +pub fn validate_status_and_disabled( + status: Option, + disabled: Option, + auth: types::ConnectorAuthType, + current_status: api_enums::ConnectorStatus, +) -> RouterResult<(api_enums::ConnectorStatus, Option)> { + let connector_status = match (status, auth) { + (Some(common_enums::ConnectorStatus::Active), types::ConnectorAuthType::TemporaryAuth) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector status cannot be active when using TemporaryAuth".to_string(), + } + .into()); + } + (Some(status), _) => status, + (None, types::ConnectorAuthType::TemporaryAuth) => common_enums::ConnectorStatus::Inactive, + (None, _) => current_status, + }; + + let disabled = match (disabled, connector_status) { + (Some(true), common_enums::ConnectorStatus::Inactive) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector cannot be enabled when connector_status is inactive or when using TemporaryAuth" + .to_string(), + } + .into()); + } + (Some(disabled), _) => Some(disabled), + (None, common_enums::ConnectorStatus::Inactive) => Some(true), + (None, _) => None, + }; + + Ok((connector_status, disabled)) +} diff --git a/crates/router/src/core/verification/utils.rs b/crates/router/src/core/verification/utils.rs index 433430507fb1..56960d3cb480 100644 --- a/crates/router/src/core/verification/utils.rs +++ b/crates/router/src/core/verification/utils.rs @@ -60,6 +60,7 @@ pub async fn check_existence_and_add_domain_to_db( applepay_verified_domains: Some(already_verified_domains.clone()), pm_auth_config: None, connector_label: None, + status: None, }; state .store diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index ecf52531f28a..4fbb8f19ccff 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -643,6 +643,7 @@ impl MerchantConnectorAccountInterface for MockDb { profile_id: t.profile_id, applepay_verified_domains: t.applepay_verified_domains, pm_auth_config: t.pm_auth_config, + status: t.status, }; accounts.push(account.clone()); account @@ -839,6 +840,7 @@ mod merchant_connector_account_cache_tests { profile_id: Some(profile_id.to_string()), applepay_verified_domains: None, pm_auth_config: None, + status: common_enums::ConnectorStatus::Inactive, }; db.insert_merchant_connector_account(mca.clone(), &merchant_key) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 095e1f45f93f..04ef90546cfa 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -174,6 +174,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::AttemptStatus, api_models::enums::CaptureStatus, api_models::enums::ReconStatus, + api_models::enums::ConnectorStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7cf8f6b71fa5..ceeb93f69763 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -900,6 +900,7 @@ pub struct ResponseRouterData { #[derive(Default, Debug, Clone, serde::Deserialize)] #[serde(tag = "auth_type")] pub enum ConnectorAuthType { + TemporaryAuth, HeaderKey { api_key: Secret, }, diff --git a/crates/router/src/types/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index 58c2e018316c..c84abbefc381 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -35,6 +35,7 @@ pub struct MerchantConnectorAccount { pub profile_id: Option, pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: enums::ConnectorStatus, } #[derive(Debug)] @@ -54,6 +55,7 @@ pub enum MerchantConnectorAccountUpdate { applepay_verified_domains: Option>, pm_auth_config: Option, connector_label: Option, + status: Option, }, } @@ -89,6 +91,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }, ) } @@ -128,6 +131,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: other.profile_id, applepay_verified_domains: other.applepay_verified_domains, pm_auth_config: other.pm_auth_config, + status: other.status, }) } @@ -155,6 +159,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }) } } @@ -177,6 +182,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, } => Self { merchant_id, connector_type, @@ -194,6 +200,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, }, } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 3ffba5aff50a..2b7ea86cf51d 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -852,6 +852,7 @@ impl TryFrom for api_models::admin::MerchantCo profile_id: item.profile_id, applepay_verified_domains: item.applepay_verified_domains, pm_auth_config: item.pm_auth_config, + status: item.status, }) } } diff --git a/migrations/2023-11-12-131143_connector-status-column/down.sql b/migrations/2023-11-12-131143_connector-status-column/down.sql new file mode 100644 index 000000000000..9463f4d77135 --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_connector_account DROP COLUMN IF EXISTS status; +DROP TYPE IF EXISTS "ConnectorStatus"; diff --git a/migrations/2023-11-12-131143_connector-status-column/up.sql b/migrations/2023-11-12-131143_connector-status-column/up.sql new file mode 100644 index 000000000000..7a992d142d6f --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here +CREATE TYPE "ConnectorStatus" AS ENUM ('active', 'inactive'); + +ALTER TABLE merchant_connector_account +ADD COLUMN status "ConnectorStatus"; + +UPDATE merchant_connector_account SET status='active'; + +ALTER TABLE merchant_connector_account +ALTER COLUMN status SET NOT NULL, +ALTER COLUMN status SET DEFAULT 'inactive'; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index be66a1bff92c..7d94f13dd125 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4147,6 +4147,13 @@ } } }, + "ConnectorStatus": { + "type": "string", + "enum": [ + "inactive", + "active" + ] + }, "ConnectorType": { "type": "string", "enum": [ @@ -6871,7 +6878,8 @@ "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ "connector_type", - "connector_name" + "connector_name", + "status" ], "properties": { "connector_type": { @@ -7002,6 +7010,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -7087,7 +7098,8 @@ "required": [ "connector_type", "connector_name", - "merchant_connector_id" + "merchant_connector_id", + "status" ], "properties": { "connector_type": { @@ -7230,6 +7242,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -7237,7 +7252,8 @@ "type": "object", "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ - "connector_type" + "connector_type", + "status" ], "properties": { "connector_type": { @@ -7335,6 +7351,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, From 644709d95f6ecaab497cf0cf3788b9e2ed88b855 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:19:02 +0530 Subject: [PATCH 08/13] fix(connector): [fiserv] fix metadata deserialization in merchant_connector_account (#2746) --- .../src/connector/fiserv/transformers.rs | 48 +++++++++++++------ crates/router/src/core/admin.rs | 1 + 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index 2d07da7f47a4..f8d88d08c6ba 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -1,4 +1,4 @@ -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, pii}; use error_stack::ResultExt; use serde::{Deserialize, Serialize}; @@ -150,9 +150,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservP merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }; let metadata = item.router_data.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; let merchant_details = MerchantDetails { merchant_id: auth.merchant_account, @@ -230,9 +232,11 @@ impl TryFrom<&types::PaymentsCancelRouterData> for FiservCancelRequest { fn try_from(item: &types::PaymentsCancelRouterData) -> Result { let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; let metadata = item.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { merchant_details: MerchantDetails { merchant_id: auth.merchant_account, @@ -418,11 +422,21 @@ pub struct ReferenceTransactionDetails { } #[derive(Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionObject { +pub struct FiservSessionObject { pub terminal_id: String, } +impl TryFrom<&Option> for FiservSessionObject { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCaptureRequest { type Error = error_stack::Report; fn try_from( @@ -434,9 +448,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCap .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), @@ -527,9 +543,11 @@ impl TryFrom<&FiservRouterData<&types::RefundsRouterData>> for FiservRefun .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index c921a9164cb0..3a0c938c32b4 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1589,6 +1589,7 @@ pub(crate) fn validate_auth_and_metadata_type( } api_enums::Connector::Fiserv => { fiserv::transformers::FiservAuthType::try_from(val)?; + fiserv::transformers::FiservSessionObject::try_from(connector_meta_data)?; Ok(()) } api_enums::Connector::Forte => { From efeebc0f2365f0900de3dd3e10a1539621c9933d Mon Sep 17 00:00:00 2001 From: Shanks Date: Mon, 20 Nov 2023 16:12:06 +0530 Subject: [PATCH 09/13] fix(router): associate parent payment token with `payment_method_id` as hyperswitch token for saved cards (#2130) Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> --- crates/router/src/core/errors.rs | 2 +- crates/router/src/core/payment_methods.rs | 77 ++++++- .../router/src/core/payment_methods/cards.rs | 77 ++++--- crates/router/src/core/payments/helpers.rs | 213 ++++++++++++------ crates/router/src/routes/payment_methods.rs | 13 +- .../src/types/storage/payment_method.rs | 40 ++++ 6 files changed, 320 insertions(+), 102 deletions(-) diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 810c079987eb..03bb9a41b5b5 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -19,7 +19,7 @@ use storage_impl::errors as storage_impl_errors; pub use user::*; pub use self::{ - api_error_response::ApiErrorResponse, + api_error_response::{ApiErrorResponse, NotImplementedMessage}, customers_error_response::CustomersErrorResponse, sch_errors::*, storage_errors::*, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index b19b381af507..0628d301796e 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -9,13 +9,17 @@ pub use api_models::{ pub use common_utils::request::RequestBody; use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::enums; +use error_stack::IntoReport; use crate::{ - core::{errors::RouterResult, payments::helpers}, + core::{ + errors::{self, RouterResult}, + payments::helpers, + }, routes::AppState, types::{ api::{self, payments}, - domain, + domain, storage, }, }; @@ -30,6 +34,14 @@ pub trait PaymentMethodRetrieve { payment_attempt: &PaymentAttempt, merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)>; + + async fn retrieve_payment_method_with_token( + state: &AppState, + key_store: &domain::MerchantKeyStore, + token: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + ) -> RouterResult>; } #[async_trait::async_trait] @@ -105,4 +117,65 @@ impl PaymentMethodRetrieve for Oss { _ => Ok((None, None)), } } + + async fn retrieve_payment_method_with_token( + state: &AppState, + merchant_key_store: &domain::MerchantKeyStore, + token_data: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + ) -> RouterResult> { + match token_data { + storage::PaymentTokenData::TemporaryGeneric(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + ) + .await + } + + storage::PaymentTokenData::Temporary(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + ) + .await + } + + storage::PaymentTokenData::Permanent(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::PermanentCard(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::AuthBankDebit(_) => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::NotImplementedMessage::Default, + }) + .into_report() + } + } + } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 80daf66a6926..f2eeedf5388f 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -50,7 +50,7 @@ use crate::{ self, types::{decrypt, encrypt_optional, AsyncLift}, }, - storage::{self, enums}, + storage::{self, enums, PaymentTokenData}, transformers::ForeignFrom, }, utils::{self, ConnectorResponseExt, OptionExt}, @@ -2103,23 +2103,32 @@ pub async fn list_customer_payment_method( let mut customer_pms = Vec::new(); for pm in resp.into_iter() { let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); - let hyperswitch_token = generate_id(consts::ID_LENGTH, "token"); - let card = if pm.payment_method == enums::PaymentMethod::Card { - get_card_details(&pm, key, state, &hyperswitch_token, &key_store).await? - } else { - None - }; + let (card, pmd, hyperswitch_token_data) = match pm.payment_method { + enums::PaymentMethod::Card => ( + Some(get_card_details(&pm, key, state).await?), + None, + PaymentTokenData::permanent_card(pm.payment_method_id.clone()), + ), - #[cfg(feature = "payouts")] - let pmd = if pm.payment_method == enums::PaymentMethod::BankTransfer { - Some( - get_lookup_key_for_payout_method(state, &key_store, &hyperswitch_token, &pm) - .await?, - ) - } else { - None + #[cfg(feature = "payouts")] + enums::PaymentMethod::BankTransfer => { + let token = generate_id(consts::ID_LENGTH, "token"); + let token_data = PaymentTokenData::temporary_generic(token.clone()); + ( + None, + Some(get_lookup_key_for_payout_method(state, &key_store, &token, &pm).await?), + token_data, + ) + } + + _ => ( + None, + None, + PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")), + ), }; + //Need validation for enabled payment method ,querying MCA let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), @@ -2134,10 +2143,7 @@ pub async fn list_customer_payment_method( installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), - #[cfg(feature = "payouts")] bank_transfer: pmd, - #[cfg(not(feature = "payouts"))] - bank_transfer: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2153,7 +2159,7 @@ pub async fn list_customer_payment_method( &parent_payment_method_token, pma.payment_method, )) - .insert(intent_created, hyperswitch_token, state) + .insert(intent_created, hyperswitch_token_data, state) .await?; if let Some(metadata) = pma.metadata { @@ -2200,10 +2206,8 @@ async fn get_card_details( pm: &payment_method::PaymentMethod, key: &[u8], state: &routes::AppState, - hyperswitch_token: &str, - key_store: &domain::MerchantKeyStore, -) -> errors::RouterResult> { - let mut _card_decrypted = +) -> errors::RouterResult { + let card_decrypted = decrypt::(pm.payment_method_data.clone(), key) .await .change_context(errors::StorageError::DecryptionError) @@ -2217,16 +2221,17 @@ async fn get_card_details( _ => None, }); - Ok(Some( - get_lookup_key_from_locker(state, hyperswitch_token, pm, key_store).await?, - )) + Ok(if let Some(mut crd) = card_decrypted { + crd.scheme = pm.scheme.clone(); + crd + } else { + get_card_details_from_locker(state, pm).await? + }) } -pub async fn get_lookup_key_from_locker( +pub async fn get_card_details_from_locker( state: &routes::AppState, - payment_token: &str, pm: &storage::PaymentMethod, - merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card = get_card_from_locker( state, @@ -2237,9 +2242,19 @@ pub async fn get_lookup_key_from_locker( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting card from card vault")?; - let card_detail = payment_methods::get_card_detail(pm, card) + + payment_methods::get_card_detail(pm, card) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Get Card Details Failed")?; + .attach_printable("Get Card Details Failed") +} + +pub async fn get_lookup_key_from_locker( + state: &routes::AppState, + payment_token: &str, + pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, +) -> errors::RouterResult { + let card_detail = get_card_details_from_locker(state, pm).await?; let card = card_detail.clone(); let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index cd056f81ebb4..fb74006a0671 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -55,7 +55,7 @@ use crate::{ utils::{ self, crypto::{self, SignMessage}, - OptionExt, + OptionExt, StringExt, }, }; @@ -1326,6 +1326,114 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( )) } +pub async fn retrieve_payment_method_with_temporary_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, + merchant_key_store: &domain::MerchantKeyStore, +) -> RouterResult> { + let (pm, supplementary_data) = + vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store) + .await + .attach_printable( + "Payment method for given token not found or there was a problem fetching it", + )?; + + utils::when( + supplementary_data + .customer_id + .ne(&payment_intent.customer_id), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + }, + )?; + + Ok::<_, error_stack::Report>(match pm { + Some(api::PaymentMethodData::Card(card)) => { + if let Some(cvc) = card_cvc { + let mut updated_card = card; + updated_card.card_cvc = cvc; + let updated_pm = api::PaymentMethodData::Card(updated_card); + vault::Vault::store_payment_method_data_in_locker( + state, + Some(token.to_owned()), + &updated_pm, + payment_intent.customer_id.to_owned(), + enums::PaymentMethod::Card, + merchant_key_store, + ) + .await?; + + Some((updated_pm, enums::PaymentMethod::Card)) + } else { + Some(( + api::PaymentMethodData::Card(card), + enums::PaymentMethod::Card, + )) + } + } + + Some(the_pm @ api::PaymentMethodData::Wallet(_)) => { + Some((the_pm, enums::PaymentMethod::Wallet)) + } + + Some(the_pm @ api::PaymentMethodData::BankTransfer(_)) => { + Some((the_pm, enums::PaymentMethod::BankTransfer)) + } + + Some(the_pm @ api::PaymentMethodData::BankRedirect(_)) => { + Some((the_pm, enums::PaymentMethod::BankRedirect)) + } + + Some(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Payment method received from locker is unsupported by locker")?, + + None => None, + }) +} + +pub async fn retrieve_card_with_permanent_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, +) -> RouterResult { + let customer_id = payment_intent + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "no customer id provided for the payment".to_string(), + })?; + + let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?; + + let api_card = api::Card { + card_number: card.card_number, + card_holder_name: card + .name_on_card + .get_required_value("name_on_card") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("card holder name was not saved in permanent locker")?, + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_cvc: card_cvc.unwrap_or_default(), + card_issuer: card.card_brand, + nick_name: card.nick_name.map(masking::Secret::new), + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }; + + Ok(api::PaymentMethodData::Card(api_card)) +} + pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( operation: BoxedOperation<'a, F, R, Ctx>, state: &'a AppState, @@ -1339,7 +1447,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let token = payment_data.token.clone(); let hyperswitch_token = match payment_data.mandate_id { - Some(_) => token, + Some(_) => token.map(storage::PaymentTokenData::temporary_generic), None => { if let Some(token) = token { let redis_conn = state @@ -1358,7 +1466,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( .get_required_value("payment_method")?, ); - let key = redis_conn + let token_data_string = redis_conn .get_key::>(&key) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1369,7 +1477,26 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( }, ))?; - Some(key) + let token_data_result = token_data_string + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data"); + + let token_data = match token_data_result { + Ok(data) => data, + Err(e) => { + // The purpose of this logic is backwards compatibility to support tokens + // in redis that might be following the old format. + if token_data_string.starts_with('{') { + return Err(e); + } else { + storage::PaymentTokenData::temporary_generic(token_data_string) + } + } + }; + + Some(token_data) } else { None } @@ -1381,72 +1508,24 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { - let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker( + let payment_method_details = Ctx::retrieve_payment_method_with_token( state, - &hyperswitch_token, merchant_key_store, + &hyperswitch_token, + &payment_data.payment_intent, + card_cvc, ) .await - .attach_printable( - "Payment method for given token not found or there was a problem fetching it", - )?; + .attach_printable("in 'make_pm_data'")?; - utils::when( - supplementary_data - .customer_id - .ne(&payment_data.payment_intent.customer_id), - || { - Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + Ok::<_, error_stack::Report>( + if let Some((payment_method_data, payment_method)) = payment_method_details { + payment_data.payment_attempt.payment_method = Some(payment_method); + Some(payment_method_data) + } else { + None }, - )?; - - Ok::<_, error_stack::Report>(match pm.clone() { - Some(api::PaymentMethodData::Card(card)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Card); - if let Some(cvc) = card_cvc { - let mut updated_card = card; - updated_card.card_cvc = cvc; - let updated_pm = api::PaymentMethodData::Card(updated_card); - vault::Vault::store_payment_method_data_in_locker( - state, - Some(hyperswitch_token), - &updated_pm, - payment_data.payment_intent.customer_id.to_owned(), - enums::PaymentMethod::Card, - merchant_key_store, - ) - .await?; - Some(updated_pm) - } else { - pm - } - } - - Some(api::PaymentMethodData::Wallet(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Wallet); - pm - } - - Some(api::PaymentMethodData::BankTransfer(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankTransfer); - pm - } - Some(api::PaymentMethodData::BankRedirect(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankRedirect); - pm - } - Some(_) => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable( - "Payment method received from locker is unsupported by locker", - )?, - - None => None, - }) + ) } (Some(_), _) => { @@ -1495,7 +1574,11 @@ pub async fn store_in_vault_and_generate_ppmt( }); if let Some(key_for_hyperswitch_token) = key_for_hyperswitch_token { key_for_hyperswitch_token - .insert(Some(payment_intent.created_at), router_token, state) + .insert( + Some(payment_intent.created_at), + storage::PaymentTokenData::temporary_generic(router_token), + state, + ) .await?; }; Ok(parent_payment_method_token) diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 83d4c7f96611..43a7272a4435 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -9,7 +9,11 @@ use super::app::AppState; use crate::{ core::{api_locking, errors, payment_methods::cards}, services::{api, authentication as auth}, - types::api::payment_methods::{self, PaymentMethodId}, + types::{ + api::payment_methods::{self, PaymentMethodId}, + storage::payment_method::PaymentTokenData, + }, + utils::Encode, }; /// PaymentMethods - Create @@ -379,9 +383,12 @@ impl ParentPaymentMethodToken { pub async fn insert( &self, intent_created_at: Option, - token: String, + token: PaymentTokenData, state: &AppState, ) -> CustomResult<(), errors::ApiErrorResponse> { + let token_json_str = Encode::::encode_to_string_of_json(&token) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to serialize hyperswitch token to json")?; let redis_conn = state .store .get_redis_conn() @@ -392,7 +399,7 @@ impl ParentPaymentMethodToken { redis_conn .set_key_with_expiry( &self.key_for_token, - token, + token_json_str, TOKEN_TTL - time_elapsed.whole_seconds(), ) .await diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 737e6f66076a..096303446dc5 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -1,4 +1,44 @@ +use api_models::payment_methods; pub use diesel_models::payment_method::{ PaymentMethod, PaymentMethodNew, PaymentMethodUpdate, PaymentMethodUpdateInternal, TokenizeCoreWorkflow, }; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PaymentTokenKind { + Temporary, + Permanent, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CardTokenData { + pub token: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenericTokenData { + pub token: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PaymentTokenData { + // The variants 'Temporary' and 'Permanent' are added for backwards compatibility + // with any tokenized data present in Redis at the time of deployment of this change + Temporary(GenericTokenData), + TemporaryGeneric(GenericTokenData), + Permanent(CardTokenData), + PermanentCard(CardTokenData), + AuthBankDebit(payment_methods::BankAccountConnectorDetails), +} + +impl PaymentTokenData { + pub fn permanent_card(token: String) -> Self { + Self::PermanentCard(CardTokenData { token }) + } + + pub fn temporary_generic(token: String) -> Self { + Self::TemporaryGeneric(GenericTokenData { token }) + } +} From 39540015fde476ad8492a9142c2c1bfda8444a27 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:35:07 +0530 Subject: [PATCH 10/13] feat(router): add unified_code, unified_message in payments response (#2918) --- crates/api_models/src/gsm.rs | 6 ++ crates/api_models/src/payments.rs | 10 +++ .../src/payments/payment_attempt.rs | 8 +++ crates/diesel_models/src/gsm.rs | 12 ++++ crates/diesel_models/src/payment_attempt.rs | 20 ++++++ crates/diesel_models/src/schema.rs | 8 +++ crates/router/src/core/gsm.rs | 4 ++ crates/router/src/core/payments/helpers.rs | 45 ++++++++++++++ .../payments/operations/payment_response.rs | 17 +++++- crates/router/src/core/payments/retry.rs | 61 ++++++------------- .../router/src/core/payments/transformers.rs | 4 ++ crates/router/src/types/transformers.rs | 6 ++ crates/router/src/workflows/payment_sync.rs | 2 + .../src/mock_db/payment_attempt.rs | 2 + .../src/payments/payment_attempt.rs | 26 ++++++++ .../down.sql | 3 + .../up.sql | 3 + .../down.sql | 3 + .../up.sql | 3 + openapi/openapi_spec.json | 44 +++++++++++++ 20 files changed, 242 insertions(+), 45 deletions(-) create mode 100644 migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql create mode 100644 migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql create mode 100644 migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql create mode 100644 migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index 254981b1f8f7..81798d05178b 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -13,6 +13,8 @@ pub struct GsmCreateRequest { pub router_error: Option, pub decision: GsmDecision, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -57,6 +59,8 @@ pub struct GsmUpdateRequest { pub router_error: Option, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -88,4 +92,6 @@ pub struct GsmResponse { pub router_error: Option, pub decision: String, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index b479f4442ba6..9f4f151c2228 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -391,6 +391,10 @@ pub struct PaymentAttemptResponse { /// reference to the payment at connector side #[schema(value_type = Option, example = "993672945374576J")] pub reference_id: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, } #[derive( @@ -2089,6 +2093,12 @@ pub struct PaymentsResponse { #[schema(example = "Failed while verifying the card")] pub error_message: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, + /// Payment Experience for the current payment #[schema(value_type = Option, example = "redirect_to_url")] pub payment_experience: Option, diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 88fc7b3b524a..1b43177feb56 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -145,6 +145,8 @@ pub struct PaymentAttempt { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -207,6 +209,8 @@ pub struct PaymentAttemptNew { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -292,6 +296,8 @@ pub enum PaymentAttemptUpdate { updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -316,6 +322,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, }, MultipleCaptureCountUpdate { multiple_capture_count: i16, diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index 2e824758aa5a..39bd880cd6c2 100644 --- a/crates/diesel_models/src/gsm.rs +++ b/crates/diesel_models/src/gsm.rs @@ -34,6 +34,8 @@ pub struct GatewayStatusMap { #[serde(with = "custom_serde::iso8601")] pub last_modified: PrimitiveDateTime, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Insertable)] @@ -48,6 +50,8 @@ pub struct GatewayStatusMappingNew { pub router_error: Option, pub decision: String, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive( @@ -71,6 +75,8 @@ pub struct GatewayStatusMapperUpdateInternal { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug)] @@ -79,6 +85,8 @@ pub struct GatewayStatusMappingUpdate { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } impl From for GatewayStatusMapperUpdateInternal { @@ -88,12 +96,16 @@ impl From for GatewayStatusMapperUpdateInternal { status, router_error, step_up_possible, + unified_code, + unified_message, } = value; Self { status, router_error, decision, step_up_possible, + unified_code, + unified_message, ..Default::default() } } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index cd976b9e19db..bb8f2b60bbb7 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -61,6 +61,8 @@ pub struct PaymentAttempt { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)] @@ -124,6 +126,8 @@ pub struct PaymentAttemptNew { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -209,6 +213,8 @@ pub enum PaymentAttemptUpdate { updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -233,6 +239,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, }, MultipleCaptureCountUpdate { multiple_capture_count: i16, @@ -298,6 +306,8 @@ pub struct PaymentAttemptUpdateInternal { merchant_connector_id: Option, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, } impl PaymentAttemptUpdate { @@ -352,6 +362,8 @@ impl PaymentAttemptUpdate { merchant_connector_id: pa_update.merchant_connector_id, authentication_data: pa_update.authentication_data.or(source.authentication_data), encoded_data: pa_update.encoded_data.or(source.encoded_data), + unified_code: pa_update.unified_code.unwrap_or(source.unified_code), + unified_message: pa_update.unified_message.unwrap_or(source.unified_message), ..source } } @@ -488,6 +500,8 @@ impl From for PaymentAttemptUpdateInternal { updated_by, authentication_data, encoded_data, + unified_code, + unified_message, } => Self { status: Some(status), connector, @@ -508,6 +522,8 @@ impl From for PaymentAttemptUpdateInternal { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, ..Default::default() }, PaymentAttemptUpdate::ErrorUpdate { @@ -518,6 +534,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => Self { connector, status: Some(status), @@ -527,6 +545,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, ..Default::default() }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 190a123185e4..ce974e409a2c 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -331,6 +331,10 @@ diesel::table! { created_at -> Timestamp, last_modified -> Timestamp, step_up_possible -> Bool, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } @@ -585,6 +589,10 @@ diesel::table! { merchant_connector_id -> Nullable, authentication_data -> Nullable, encoded_data -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs index ed72275a73ab..611a35d63632 100644 --- a/crates/router/src/core/gsm.rs +++ b/crates/router/src/core/gsm.rs @@ -65,6 +65,8 @@ pub async fn update_gsm_rule( status, router_error, step_up_possible, + unified_code, + unified_message, } = gsm_request; GsmInterface::update_gsm_rule( db, @@ -78,6 +80,8 @@ pub async fn update_gsm_rule( status, router_error: Some(router_error), step_up_possible, + unified_code, + unified_message, }, ) .await diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index fb74006a0671..ae729ff8fa25 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3026,6 +3026,8 @@ impl AttemptType { authentication_data: None, encoded_data: None, merchant_connector_id: None, + unified_code: None, + unified_message: None, } } @@ -3516,3 +3518,46 @@ pub fn validate_payment_link_request( } Ok(()) } + +pub async fn get_gsm_record( + state: &AppState, + error_code: Option, + error_message: Option, + connector_name: String, + flow: String, +) -> Option { + let get_gsm = || async { + state.store.find_gsm_rule( + connector_name.clone(), + flow.clone(), + "sub_flow".to_string(), + error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response + error_message.clone().unwrap_or_default(), + ) + .await + .map_err(|err| { + if err.current_context().is_db_not_found() { + logger::warn!( + "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", + connector_name, + flow, + error_code, + error_message + ); + metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); + } else { + metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); + }; + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch decision from gsm") + }) + }; + get_gsm() + .await + .map_err(|err| { + // warn log should suffice here because we are not propagating this error + logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); + err + }) + .ok() +} diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 1cfc37efa449..083d1bb030dd 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -16,7 +16,7 @@ use crate::{ errors::{self, RouterResult, StorageErrorExt}, mandate, payment_methods::PaymentMethodRetrieve, - payments::{types::MultipleCaptureData, PaymentData}, + payments::{helpers as payments_helpers, types::MultipleCaptureData, PaymentData}, utils as core_utils, }, routes::{metrics, AppState}, @@ -331,7 +331,16 @@ async fn payment_response_update_tracker( (Some((multiple_capture_data, capture_update_list)), None) } None => { + let connector_name = router_data.connector.to_string(); let flow_name = core_utils::get_flow_name::()?; + let option_gsm = payments_helpers::get_gsm_record( + state, + Some(err.code.clone()), + Some(err.message.clone()), + connector_name, + flow_name.clone(), + ) + .await; let status = // mark previous attempt status for technical failures in PSync flow if flow_name == "PSync" { @@ -364,6 +373,8 @@ async fn payment_response_update_tracker( None }, updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), }), ) } @@ -470,7 +481,9 @@ async fn payment_response_update_tracker( payment_token: None, error_code: error_status.clone(), error_message: error_status.clone(), - error_reason: error_status, + error_reason: error_status.clone(), + unified_code: error_status.clone(), + unified_message: error_status, connector_response_reference_id, amount_capturable: if router_data.status.is_terminal_status() || router_data diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 788e83b05e37..3c0106206e1d 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -55,7 +55,7 @@ where metrics::AUTO_RETRY_ELIGIBLE_REQUEST_COUNT.add(&metrics::CONTEXT, 1, &[]); - let mut initial_gsm = get_gsm(state, &router_data).await; + let mut initial_gsm = get_gsm(state, &router_data).await?; //Check if step-up to threeDS is possible and merchant has enabled let step_up_possible = initial_gsm @@ -99,7 +99,7 @@ where // Use initial_gsm for first time alone let gsm = match initial_gsm.as_ref() { Some(gsm) => Some(gsm.clone()), - None => get_gsm(state, &router_data).await, + None => get_gsm(state, &router_data).await?, }; match get_gsm_decision(gsm) { @@ -214,46 +214,16 @@ pub async fn get_retries( pub async fn get_gsm( state: &app::AppState, router_data: &types::RouterData, -) -> Option { +) -> RouterResult> { let error_response = router_data.response.as_ref().err(); let error_code = error_response.map(|err| err.code.to_owned()); let error_message = error_response.map(|err| err.message.to_owned()); - let get_gsm = || async { - let connector_name = router_data.connector.to_string(); - let flow = get_flow_name::()?; - state.store.find_gsm_rule( - connector_name.clone(), - flow.clone(), - "sub_flow".to_string(), - error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response - error_message.clone().unwrap_or_default(), - ) - .await - .map_err(|err| { - if err.current_context().is_db_not_found() { - logger::warn!( - "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", - connector_name, - flow, - error_code, - error_message - ); - metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); - } else { - metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); - }; - err.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch decision from gsm") - }) - }; - get_gsm() - .await - .map_err(|err| { - // warn log should suffice here because we are not propagating this error - logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); - err - }) - .ok() + let connector_name = router_data.connector.to_string(); + let flow = get_flow_name::()?; + Ok( + payments::helpers::get_gsm_record(state, error_code, error_message, connector_name, flow) + .await, + ) } #[instrument(skip_all)] @@ -417,6 +387,8 @@ where updated_by: storage_scheme.to_string(), authentication_data, encoded_data, + unified_code: None, + unified_message: None, }, storage_scheme, ) @@ -427,17 +399,20 @@ where logger::error!("unexpected response: this response was not expected in Retry flow"); return Ok(()); } - Err(error_response) => { + Err(ref error_response) => { + let option_gsm = get_gsm(state, &router_data).await?; db.update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), storage::PaymentAttemptUpdate::ErrorUpdate { connector: None, - error_code: Some(Some(error_response.code)), - error_message: Some(Some(error_response.message)), + error_code: Some(Some(error_response.code.clone())), + error_message: Some(Some(error_response.message.clone())), status: storage_enums::AttemptStatus::Failure, - error_reason: Some(error_response.reason), + error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), }, storage_scheme, ) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6c6b4ae9339f..f395c023128c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -685,6 +685,8 @@ where .set_profile_id(payment_intent.profile_id) .set_attempt_count(payment_intent.attempt_count) .set_merchant_connector_id(payment_attempt.merchant_connector_id) + .set_unified_code(payment_attempt.unified_code) + .set_unified_message(payment_attempt.unified_message) .to_owned(), headers, )) @@ -745,6 +747,8 @@ where attempt_count: payment_intent.attempt_count, payment_link: payment_link_data, surcharge_details, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, ..Default::default() }, headers, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2b7ea86cf51d..b73ba0964fbf 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -878,6 +878,8 @@ impl ForeignFrom for api_models::payments::PaymentAttem payment_experience: payment_attempt.payment_experience, payment_method_type: payment_attempt.payment_method_type, reference_id: payment_attempt.connector_response_reference_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, } } } @@ -1055,6 +1057,8 @@ impl ForeignFrom for storage::GatewayStatusMapp status: value.status, router_error: value.router_error, step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, } } } @@ -1071,6 +1075,8 @@ impl ForeignFrom for gsm_api_types::GsmResponse { status: value.status, router_error: value.router_error, step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, } } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 00e7357d896f..43e327559a0c 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -136,6 +136,8 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { )), amount_capturable: Some(0), updated_by: merchant_account.storage_scheme.to_string(), + unified_code: None, + unified_message: None, }; payment_data.payment_attempt = db diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index cb2f81daa797..fe244b10325f 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -144,6 +144,8 @@ impl PaymentAttemptInterface for MockDb { authentication_data: payment_attempt.authentication_data, encoded_data: payment_attempt.encoded_data, merchant_connector_id: payment_attempt.merchant_connector_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 3d00e2f2bf7a..cb74c981ea71 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -364,6 +364,8 @@ impl PaymentAttemptInterface for KVRouterStore { authentication_data: payment_attempt.authentication_data.clone(), encoded_data: payment_attempt.encoded_data.clone(), merchant_connector_id: payment_attempt.merchant_connector_id.clone(), + unified_code: payment_attempt.unified_code.clone(), + unified_message: payment_attempt.unified_message.clone(), }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -966,6 +968,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1018,6 +1022,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1070,6 +1076,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1120,6 +1128,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1255,6 +1265,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1274,6 +1286,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, Self::UnresolvedResponseUpdate { status, @@ -1307,6 +1321,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1315,6 +1331,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, }, Self::MultipleCaptureCountUpdate { multiple_capture_count, @@ -1504,6 +1522,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => Self::ResponseUpdate { status, connector, @@ -1523,6 +1543,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -1556,6 +1578,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => Self::ErrorUpdate { connector, status, @@ -1564,6 +1588,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, }, DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { multiple_capture_count, diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql new file mode 100644 index 000000000000..9561c8509b69 --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_code; +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql new file mode 100644 index 000000000000..a4b1250a032a --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql new file mode 100644 index 000000000000..83609093e136 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_code; +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql new file mode 100644 index 000000000000..5e390d51f760 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 7d94f13dd125..65280c187142 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5934,6 +5934,14 @@ }, "step_up_possible": { "type": "boolean" + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -6039,6 +6047,14 @@ }, "step_up_possible": { "type": "boolean" + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -6113,6 +6129,14 @@ "step_up_possible": { "type": "boolean", "nullable": true + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -8155,6 +8179,16 @@ "description": "reference to the payment at connector side", "example": "993672945374576J", "nullable": true + }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true } } }, @@ -10041,6 +10075,16 @@ "example": "Failed while verifying the card", "nullable": true }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, "payment_experience": { "allOf": [ { From 44deeb7e7605cb5320b84c0fac1fd551877803a4 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:19:19 +0530 Subject: [PATCH 11/13] refactor(core): query business profile only once (#2830) --- crates/router/src/core/payments.rs | 64 +++--- crates/router/src/core/payments/operations.rs | 19 +- .../payments/operations/payment_approve.rs | 114 ++++++---- .../payments/operations/payment_cancel.rs | 100 +++++---- .../payments/operations/payment_capture.rs | 99 ++++---- .../operations/payment_complete_authorize.rs | 113 ++++++---- .../payments/operations/payment_confirm.rs | 212 +++++++++++------- .../payments/operations/payment_create.rs | 103 +++++---- .../payments/operations/payment_reject.rs | 103 +++++---- .../payments/operations/payment_session.rs | 99 ++++---- .../core/payments/operations/payment_start.rs | 99 ++++---- .../payments/operations/payment_status.rs | 127 ++++++----- .../payments/operations/payment_update.rs | 99 ++++---- 13 files changed, 772 insertions(+), 579 deletions(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 000cadec0091..0259c48ee827 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -31,9 +31,8 @@ use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, ut use time; pub use self::operations::{ - PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, - PaymentMethodValidate, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, - PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentReject, + PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, }; use self::{ flows::{ConstructFlowSpecificData, Feature}, @@ -112,7 +111,12 @@ where tracing::Span::current().record("payment_id", &format!("{}", validate_result.payment_id)); - let (operation, mut payment_data, customer_details) = operation + let operations::GetTrackerResponse { + operation, + customer_details, + mut payment_data, + business_profile, + } = operation .to_get_tracker()? .get_trackers( state, @@ -142,6 +146,7 @@ where state, &req, &merchant_account, + &business_profile, &key_store, &mut payment_data, eligible_connectors, @@ -1998,11 +2003,13 @@ where Ok(()) } +#[allow(clippy::too_many_arguments)] pub async fn get_connector_choice( operation: &BoxedOperation<'_, F, Req, Ctx>, state: &AppState, req: &Req, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, eligible_connectors: Option>, @@ -2040,6 +2047,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, Some(straight_through), @@ -2052,6 +2060,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, None, @@ -2075,6 +2084,7 @@ where pub async fn connector_selection( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2114,6 +2124,7 @@ where let decided_connector = decide_connector( state.clone(), merchant_account, + business_profile, key_store, payment_data, request_straight_through, @@ -2141,9 +2152,11 @@ where Ok(decided_connector) } +#[allow(clippy::too_many_arguments)] pub async fn decide_connector( state: AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2345,6 +2358,7 @@ where route_connector_v1( &state, merchant_account, + business_profile, key_store, payment_data, routing_data, @@ -2480,6 +2494,7 @@ where pub async fn route_connector_v1( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, routing_data: &mut storage::RoutingData, @@ -2488,44 +2503,19 @@ pub async fn route_connector_v1( where F: Send + Clone, { - #[cfg(not(feature = "business_profile_routing"))] - let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + let routing_algorithm = if cfg!(feature = "business_profile_routing") { + business_profile.routing_algorithm.clone() + } else { + merchant_account.routing_algorithm.clone() + }; + + let algorithm_ref = routing_algorithm + .map(|ra| ra.parse_value::("RoutingAlgorithmRef")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Could not decode merchant routing algorithm ref")? .unwrap_or_default(); - #[cfg(feature = "business_profile_routing")] - let algorithm_ref: api::routing::RoutingAlgorithmRef = { - let profile_id = payment_data - .payment_intent - .profile_id - .as_ref() - .get_required_value("profile_id") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("'profile_id' not set in payment intent")?; - - let business_profile = state - .store - .find_business_profile_by_profile_id(profile_id) - .await - .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { - id: profile_id.to_string(), - })?; - - business_profile - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not decode merchant routing algorithm ref")? - .unwrap_or_default() - }; - let connectors = routing::perform_static_routing_v1( state, &merchant_account.merchant_id, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index f65e65459e00..6f01c653084f 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -4,7 +4,6 @@ pub mod payment_capture; pub mod payment_complete_authorize; pub mod payment_confirm; pub mod payment_create; -pub mod payment_method_validate; pub mod payment_reject; pub mod payment_response; pub mod payment_session; @@ -20,10 +19,9 @@ use router_env::{instrument, tracing}; pub use self::{ payment_approve::PaymentApprove, payment_cancel::PaymentCancel, payment_capture::PaymentCapture, payment_confirm::PaymentConfirm, - payment_create::PaymentCreate, payment_method_validate::PaymentMethodValidate, - payment_reject::PaymentReject, payment_response::PaymentResponse, - payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, - payment_update::PaymentUpdate, + payment_create::PaymentCreate, payment_reject::PaymentReject, + payment_response::PaymentResponse, payment_session::PaymentSession, + payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ @@ -91,8 +89,15 @@ pub trait ValidateRequest { ) -> RouterResult<(BoxedOperation<'b, F, R, Ctx>, ValidateResult<'a>)>; } +pub struct GetTrackerResponse<'a, F: Clone, R, Ctx> { + pub operation: BoxedOperation<'a, F, R, Ctx>, + pub customer_details: Option, + pub payment_data: PaymentData, + pub business_profile: storage::business_profile::BusinessProfile, +} + #[async_trait] -pub trait GetTracker: Send { +pub trait GetTracker: Send { #[allow(clippy::too_many_arguments)] async fn get_trackers<'a>( &'a self, @@ -103,7 +108,7 @@ pub trait GetTracker: Send { merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<(BoxedOperation<'a, F, R, Ctx>, D, Option)>; + ) -> RouterResult>; } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 538e65e4b22e..78eb3fb1f10d 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -45,11 +45,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -76,6 +72,21 @@ impl "confirm", )?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let ( token, payment_method, @@ -207,50 +218,57 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 535edf736ca6..096f900e7195 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -12,7 +12,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, routes::AppState, services, @@ -42,11 +42,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -128,45 +124,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index ff51a2c49d77..09e79064dc69 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -41,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, - payments::PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -172,44 +168,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - payments::PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - force_sync: None, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + force_sync: None, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index c648d95a4950..7cc1edf17fd1 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -202,50 +198,71 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index afb7f110ed5d..a040782d83cd 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -50,11 +50,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -65,7 +61,6 @@ impl .change_context(errors::ApiErrorResponse::PaymentNotFound)?; // Stage 1 - let store = state.clone().store; let m_merchant_id = merchant_id.clone(); let payment_intent_fut = tokio::spawn( @@ -137,8 +132,29 @@ impl let customer_details = helpers::get_customer_details_from_request(request); // Stage 2 - let attempt_id = payment_intent.active_attempt.get_id(); + let profile_id = payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let store = state.store.clone(); + + let business_profile_fut = tokio::spawn(async move { + store + .find_business_profile_by_profile_id(&profile_id) + .map(|business_profile_result| { + business_profile_result.to_not_found_response( + errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + }, + ) + }) + .await + }); + let store = state.clone().store; let m_payment_id = payment_intent.payment_id.clone(); let m_merchant_id = merchant_id.clone(); @@ -235,48 +251,72 @@ impl .in_current_span(), ); - let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { - api_models::enums::IntentStatus::RequiresCustomerAction - | api_models::enums::IntentStatus::RequiresMerchantAction - | api_models::enums::IntentStatus::RequiresPaymentMethod - | api_models::enums::IntentStatus::RequiresConfirmation => { - let (payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( - utils::flatten_join_error(payment_attempt_fut), - utils::flatten_join_error(shipping_address_fut), - utils::flatten_join_error(billing_address_fut), - utils::flatten_join_error(config_update_fut) - )?; - - (payment_attempt, shipping_address, billing_address) - } - _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( - utils::flatten_join_error(payment_attempt_fut), - utils::flatten_join_error(shipping_address_fut), - utils::flatten_join_error(billing_address_fut), - utils::flatten_join_error(config_update_fut) - )?; - - let attempt_type = helpers::get_attempt_type( - &payment_intent, - &payment_attempt, - request, - "confirm", - )?; - - (payment_intent, payment_attempt) = attempt_type - .modify_payment_intent_and_payment_attempt( - request, - payment_intent, + // Based on whether a retry can be performed or not, fetch relevant entities + let (mut payment_attempt, shipping_address, billing_address, business_profile) = + match payment_intent.status { + api_models::enums::IntentStatus::RequiresCustomerAction + | api_models::enums::IntentStatus::RequiresMerchantAction + | api_models::enums::IntentStatus::RequiresPaymentMethod + | api_models::enums::IntentStatus::RequiresConfirmation => { + // Normal payment + let (payment_attempt, shipping_address, billing_address, business_profile, _) = + tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + ( payment_attempt, - &*state.store, - storage_scheme, + shipping_address, + billing_address, + business_profile, ) - .await?; + } + _ => { + // Retry payment + let ( + mut payment_attempt, + shipping_address, + billing_address, + business_profile, + _, + ) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + let attempt_type = helpers::get_attempt_type( + &payment_intent, + &payment_attempt, + request, + "confirm", + )?; + + // 3 + (payment_intent, payment_attempt) = attempt_type + .modify_payment_intent_and_payment_attempt( + request, + payment_intent, + payment_attempt, + &*state.store, + storage_scheme, + ) + .await?; - (payment_attempt, shipping_address, billing_address) - } - }; + ( + payment_attempt, + shipping_address, + billing_address, + business_profile, + ) + } + }; payment_intent.order_details = request .get_order_details_as_value() @@ -382,6 +422,7 @@ impl sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm }); + Self::validate_request_surcharge_details_with_session_surcharge_details( state, &payment_attempt, @@ -394,44 +435,49 @@ impl &payment_attempt, ); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 1fd4c7014c35..526b03137bea 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -53,11 +53,7 @@ impl merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; let merchant_id = &merchant_account.merchant_id; @@ -196,6 +192,20 @@ impl payment_id: payment_id.clone(), })?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let mandate_id = request .mandate_id .as_ref() @@ -246,6 +256,7 @@ impl request.confirm, self, ); + let creds_identifier = request .merchant_connector_details .as_ref() @@ -265,9 +276,8 @@ impl .transpose()?; // The operation merges mandate data from both request and payment_attempt - let setup_mandate: Option = setup_mandate.map(Into::into); + let setup_mandate = setup_mandate.map(MandateData::from); - // populate payment_data.surcharge_details from request let surcharge_details = request.surcharge_details.map(|surcharge_details| { payment_methods::SurchargeDetailsResponse { surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), @@ -280,44 +290,49 @@ impl } }); - Ok(( - operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - refunds: vec![], - disputes: vec![], - attempts: None, - force_sync: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + refunds: vec![], + disputes: vec![], + attempts: None, + force_sync: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 16d264c001ec..ae02dde4bc06 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -11,7 +11,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, routes::AppState, services, @@ -41,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, PaymentsRejectRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -114,45 +110,64 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 3abde60c2e9b..cea6eb176672 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -43,11 +43,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsSessionRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let payment_id = payment_id .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; @@ -152,44 +148,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - token: None, - setup_mandate: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + token: None, + setup_mandate: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 17f39d5150bb..6d4281216b4f 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -42,11 +42,7 @@ impl merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, payment_attempt, currency, amount); let db = &*state.store; @@ -126,44 +122,63 @@ impl ..CustomerDetails::default() }; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: payment_attempt.payment_token.clone(), - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(payment_attempt.confirm), - payment_attempt, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: payment_attempt.payment_token.clone(), + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: Some(payment_attempt.confirm), + payment_attempt, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index fb58aeb34e07..b31c406f0ecd 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -190,11 +190,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> + { get_tracker_for_sync( payment_id, merchant_account, @@ -221,12 +218,8 @@ async fn get_tracker_for_sync< request: &api::PaymentsRetrieveRequest, operation: Op, storage_scheme: enums::MerchantStorageScheme, -) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, -)> { - let (payment_intent, mut payment_attempt, currency, amount); +) -> RouterResult> { + let (payment_intent, payment_attempt, currency, amount); (payment_intent, payment_attempt) = get_payment_intent_payment_attempt( db, @@ -250,7 +243,6 @@ async fn get_tracker_for_sync< let payment_id_str = payment_attempt.payment_id.clone(); - payment_attempt.encoded_data = request.param.clone(); currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); @@ -357,53 +349,74 @@ async fn get_tracker_for_sync< }) .await .transpose()?; - Ok(( - Box::new(operation), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: payment_attempt.mandate_id.clone().map(|id| { - api_models::payments::MandateIds { - mandate_id: id, - mandate_reference_id: None, - } + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: payment_attempt + .mandate_id + .clone() + .map(|id| api_models::payments::MandateIds { + mandate_id: id, + mandate_reference_id: None, }), - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(request.force_sync), - payment_method_data: None, - force_sync: Some( - request.force_sync - && (helpers::check_force_psync_precondition(&payment_attempt.status) - || contains_encoded_data), - ), - payment_attempt, - refunds, - disputes, - attempts, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - payment_link_data: None, - surcharge_details: None, - frm_message: frm_response.ok(), + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: Some(request.force_sync), + payment_method_data: None, + force_sync: Some( + request.force_sync + && (helpers::check_force_psync_precondition(&payment_attempt.status) + || contains_encoded_data), + ), + payment_attempt, + refunds, + disputes, + attempts, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + payment_link_data: None, + surcharge_details: None, + frm_message: frm_response.ok(), + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(operation), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } impl diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 53a768f26810..6833a6a392e7 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency); let payment_id = payment_id @@ -304,48 +300,67 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) }); - Ok(( - next_operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - token, - setup_mandate, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + token, + setup_mandate, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: next_operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } From 922dc90019deeab4afcd65e6bc1d1c749bdaa09d Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:29:51 +0530 Subject: [PATCH 12/13] ci(hotfix-pr-check): use env input from GitHub to read PR body (#2923) --- .github/workflows/hotfix-pr-check.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hotfix-pr-check.yml b/.github/workflows/hotfix-pr-check.yml index 59e0bbee3cb4..7a724b602586 100644 --- a/.github/workflows/hotfix-pr-check.yml +++ b/.github/workflows/hotfix-pr-check.yml @@ -19,8 +19,9 @@ jobs: - name: Get hotfix pull request body shell: bash - run: | - echo '${{ github.event.pull_request.body }}' > hotfix_pr_body.txt + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: echo $PR_BODY > hotfix_pr_body.txt - name: Get a list of all original PR numbers shell: bash From cfabfa60db4d275066be72ee64153a34d38f13b8 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Mon, 20 Nov 2023 20:52:56 +0530 Subject: [PATCH 13/13] fix: api lock on PaymentsCreate (#2916) --- crates/api_models/src/payments.rs | 14 +++++++ .../payments/operations/payment_approve.rs | 20 ++++------ .../operations/payment_complete_authorize.rs | 19 ++++----- .../payments/operations/payment_confirm.rs | 23 +++++------ .../payments/operations/payment_create.rs | 15 ++----- .../payments/operations/payment_update.rs | 19 ++++----- crates/router/src/routes/payments.rs | 39 +++++++++++++++++-- 7 files changed, 87 insertions(+), 62 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 9f4f151c2228..c427088d688d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1679,6 +1679,20 @@ impl std::fmt::Display for PaymentIdType { } } +impl PaymentIdType { + pub fn and_then(self, f: F) -> Result + where + F: FnOnce(String) -> Result, + { + match self { + Self::PaymentIntentId(s) => f(s).map(Self::PaymentIntentId), + Self::ConnectorTransactionId(s) => f(s).map(Self::ConnectorTransactionId), + Self::PaymentAttemptId(s) => f(s).map(Self::PaymentAttemptId), + Self::PreprocessingId(s) => f(s).map(Self::PreprocessingId), + } + } +} + impl Default for PaymentIdType { fn default() -> Self { Self::PaymentIntentId(Default::default()) diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 78eb3fb1f10d..af52105c85d5 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use data_models::mandates::MandateData; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -399,15 +399,6 @@ impl ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; - let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) .change_context(errors::ApiErrorResponse::InvalidDataFormat { @@ -419,13 +410,18 @@ impl ValidateRequest ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -394,13 +390,14 @@ impl ValidateRequest ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -840,14 +832,19 @@ impl ValidateRequest ValidateRequest Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request.payment_id.clone().ok_or(error_stack::report!( + errors::ApiErrorResponse::PaymentNotFound + ))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -555,8 +550,6 @@ impl ValidateRequest ValidateRequest ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -635,13 +631,14 @@ impl ValidateRequest, ) -> impl Responder { let flow = Flow::PaymentsCreate; - let payload = json_payload.into_inner(); + let mut payload = json_payload.into_inner(); if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method { return http_not_implemented(); }; + if let Err(err) = get_or_generate_payment_id(&mut payload) { + return api::log_and_return_error_response(err); + } + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -959,6 +967,29 @@ where } } +pub fn get_or_generate_payment_id( + payload: &mut payment_types::PaymentsRequest, +) -> errors::RouterResult<()> { + let given_payment_id = payload + .payment_id + .clone() + .map(|payment_id| { + payment_id + .get_payment_intent_id() + .map_err(|err| err.change_context(errors::ApiErrorResponse::PaymentNotFound)) + }) + .transpose()?; + + let payment_id = + core_utils::get_or_generate_id("payment_id", &given_payment_id, "pay").into_report()?; + + payload.payment_id = Some(api_models::payments::PaymentIdType::PaymentIntentId( + payment_id, + )); + + Ok(()) +} + impl GetLockingInput for payment_types::PaymentsRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction where