From be902ffa5328d32efe70c40c36f86d8fbfa01c79 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:22:09 +0530 Subject: [PATCH] feat(payment_methods_v2): Payment method Create API (#5812) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/Cargo.toml | 2 +- crates/api_models/src/events.rs | 18 + crates/api_models/src/payment_methods.rs | 103 +- crates/common_utils/Cargo.toml | 1 + crates/common_utils/src/events.rs | 2 + crates/common_utils/src/id_type.rs | 4 + crates/common_utils/src/id_type/global_id.rs | 2 + .../src/id_type/payment_methods.rs | 85 ++ crates/diesel_models/src/payment_method.rs | 8 +- .../diesel_models/src/query/payment_method.rs | 5 +- .../src/payment_methods.rs | 4 +- crates/router/Cargo.toml | 2 +- crates/router/src/consts.rs | 12 + crates/router/src/core/errors.rs | 6 + crates/router/src/core/payment_methods.rs | 1005 ++++++++++++++++- .../router/src/core/payment_methods/cards.rs | 750 +++--------- .../src/core/payment_methods/transformers.rs | 132 ++- crates/router/src/core/payments.rs | 39 + crates/router/src/core/payments/helpers.rs | 67 ++ .../payments/operations/payment_create.rs | 29 + .../payments/operations/payment_response.rs | 37 + .../payments/operations/payment_status.rs | 22 + crates/router/src/core/pm_auth.rs | 11 + crates/router/src/db/kafka_store.rs | 17 + crates/router/src/db/payment_method.rs | 169 +-- crates/router/src/routes/app.rs | 18 + crates/router/src/routes/payment_methods.rs | 121 +- crates/router/src/types.rs | 1 + crates/router/src/types/api/mandates.rs | 70 +- .../router/src/types/api/payment_methods.rs | 72 +- crates/router/src/types/payment_methods.rs | 103 ++ .../src/types/storage/payment_method.rs | 29 + crates/router/src/types/transformers.rs | 14 +- .../workflows/payment_method_status_update.rs | 4 + crates/storage_impl/src/lib.rs | 2 +- 35 files changed, 2168 insertions(+), 798 deletions(-) create mode 100644 crates/common_utils/src/id_type/payment_methods.rs create mode 100644 crates/router/src/types/payment_methods.rs diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 00c12b9255..c16b1d9a41 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -20,7 +20,7 @@ v1 = ["common_utils/v1"] v2 = ["common_utils/v2", "customer_v2"] customer_v2 = ["common_utils/customer_v2"] payment_v2 = [] -payment_methods_v2 = [] +payment_methods_v2 = ["common_utils/payment_methods_v2"] [dependencies] actix-web = { version = "4.5.1", optional = true } diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 58727a46cf..13966c464d 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -152,3 +152,21 @@ impl ApiEventMetric for MetricsResponse { Some(ApiEventsType::Miscellaneous) } } + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl ApiEventMetric for PaymentMethodIntentConfirmInternal { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.id.clone(), + payment_method: Some(self.payment_method), + payment_method_type: Some(self.payment_method_type), + }) + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl ApiEventMetric for PaymentMethodIntentCreate { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethodCreate) + } +} diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index c6cf638015..6c93b0bb5b 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -160,8 +160,68 @@ pub struct PaymentMethodIntentConfirm { /// Payment method data to be passed pub payment_method_data: PaymentMethodCreateData, + + /// The type of payment method use for the payment. + #[schema(value_type = PaymentMethod,example = "card")] + pub payment_method: api_enums::PaymentMethod, + + /// This is a sub-category of payment method. + #[schema(value_type = PaymentMethodType,example = "credit")] + pub payment_method_type: api_enums::PaymentMethodType, } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl PaymentMethodIntentConfirm { + pub fn validate_payment_method_data_against_payment_method( + payment_method: api_enums::PaymentMethod, + payment_method_data: PaymentMethodCreateData, + ) -> bool { + match payment_method { + api_enums::PaymentMethod::Card => { + matches!(payment_method_data, PaymentMethodCreateData::Card(_)) + } + _ => false, + } + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct PaymentMethodIntentConfirmInternal { + #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] + pub id: String, + /// The type of payment method use for the payment. + #[schema(value_type = PaymentMethod,example = "card")] + pub payment_method: api_enums::PaymentMethod, + + /// This is a sub-category of payment method. + #[schema(value_type = PaymentMethodType,example = "credit")] + pub payment_method_type: api_enums::PaymentMethodType, + + /// For SDK based calls, client_secret would be required + pub client_secret: String, + + /// The unique identifier of the customer. + #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] + pub customer_id: Option, + + /// Payment method data to be passed + pub payment_method_data: PaymentMethodCreateData, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for PaymentMethodIntentConfirm { + fn from(item: PaymentMethodIntentConfirmInternal) -> Self { + Self { + client_secret: item.client_secret, + payment_method: item.payment_method, + payment_method_type: item.payment_method_type, + customer_id: item.customer_id, + payment_method_data: item.payment_method_data.clone(), + } + } +} #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] /// This struct is only used by and internal api to migrate payment method pub struct PaymentMethodMigrate { @@ -276,11 +336,16 @@ impl PaymentMethodCreate { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl PaymentMethodCreate { - pub fn get_payment_method_create_from_payment_method_migrate( - _card_number: CardNumber, - _payment_method_migrate: &PaymentMethodMigrate, - ) -> Self { - todo!() + pub fn validate_payment_method_data_against_payment_method( + payment_method: api_enums::PaymentMethod, + payment_method_data: PaymentMethodCreateData, + ) -> bool { + match payment_method { + api_enums::PaymentMethod::Card => { + matches!(payment_method_data, PaymentMethodCreateData::Card(_)) + } + _ => false, + } } } @@ -332,12 +397,6 @@ pub enum PaymentMethodUpdateData { #[serde(rename = "payment_method_data")] pub enum PaymentMethodCreateData { Card(CardDetail), - #[cfg(feature = "payouts")] - #[schema(value_type = Bank)] - BankTransfer(payouts::Bank), - #[cfg(feature = "payouts")] - #[schema(value_type = Wallet)] - Wallet(payouts::Wallet), } #[cfg(all( @@ -577,9 +636,6 @@ impl CardDetailUpdate { #[serde(rename = "payment_method_data")] pub enum PaymentMethodResponseData { Card(CardDetailFromLocker), - #[cfg(feature = "payouts")] - #[schema(value_type = Bank)] - Bank(payouts::Bank), } #[cfg(all( @@ -934,6 +990,25 @@ impl From for CardDetailFromLocker { } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for CardDetailsPaymentMethod { + fn from(item: CardDetail) -> Self { + Self { + issuer_country: item.card_issuing_country.map(|c| c.to_string()), + last4_digits: Some(item.card_number.get_last4()), + expiry_month: Some(item.card_exp_month), + expiry_year: Some(item.card_exp_year), + card_holder_name: item.card_holder_name, + nick_name: item.nick_name, + card_isin: None, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type.map(|card| card.to_string()), + saved_to_locker: true, + } + } +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index c6e29801b2..fdc9670045 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -21,6 +21,7 @@ payouts = ["common_enums/payouts"] v1 = [] v2 = [] customer_v2 = [] +payment_methods_v2 = [] [dependencies] async-trait = { version = "0.1.79", optional = true } diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 1e752a0f9b..16d0fd9b30 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -27,6 +27,8 @@ pub enum ApiEventsType { payment_method: Option, payment_method_type: Option, }, + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + PaymentMethodCreate, #[cfg(all(feature = "v2", feature = "customer_v2"))] Customer { id: String, diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 1473881183..20dc423310 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -12,6 +12,8 @@ mod profile; mod routing; mod global_id; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +mod payment_methods; pub use customer::CustomerId; use diesel::{ @@ -25,6 +27,8 @@ pub use merchant::MerchantId; pub use merchant_connector_account::MerchantConnectorAccountId; pub use organization::OrganizationId; pub use payment::PaymentId; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub use payment_methods::GlobalPaymentMethodId; pub use profile::ProfileId; pub use routing::RoutingId; use serde::{Deserialize, Serialize}; diff --git a/crates/common_utils/src/id_type/global_id.rs b/crates/common_utils/src/id_type/global_id.rs index f04bfc3f92..8efbc3636f 100644 --- a/crates/common_utils/src/id_type/global_id.rs +++ b/crates/common_utils/src/id_type/global_id.rs @@ -21,6 +21,7 @@ pub(crate) struct GlobalId(LengthId) pub(crate) enum GlobalEntity { Customer, Payment, + PaymentMethod, } impl GlobalEntity { @@ -28,6 +29,7 @@ impl GlobalEntity { match self { Self::Customer => "cus", Self::Payment => "pay", + Self::PaymentMethod => "pm", } } } diff --git a/crates/common_utils/src/id_type/payment_methods.rs b/crates/common_utils/src/id_type/payment_methods.rs new file mode 100644 index 0000000000..f5cc215091 --- /dev/null +++ b/crates/common_utils/src/id_type/payment_methods.rs @@ -0,0 +1,85 @@ +use diesel::{backend::Backend, deserialize::FromSql, serialize::ToSql, sql_types}; +use error_stack::ResultExt; + +use crate::{ + errors, + errors::CustomResult, + id_type::global_id::{CellId, GlobalEntity, GlobalId, GlobalIdError}, +}; + +#[derive( + Debug, + Clone, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + diesel::expression::AsExpression, +)] +#[diesel(sql_type = diesel::sql_types::Text)] +pub struct GlobalPaymentMethodId(GlobalId); + +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum GlobalPaymentMethodIdError { + #[error("Failed to construct GlobalPaymentMethodId")] + ConstructionError, +} + +impl GlobalPaymentMethodId { + fn get_global_id(&self) -> &GlobalId { + &self.0 + } + /// Create a new GlobalPaymentMethodId from celll id information + pub fn generate(cell_id: &str) -> error_stack::Result { + let cell_id = CellId::from_string(cell_id.to_string())?; + let global_id = GlobalId::generate(cell_id, GlobalEntity::PaymentMethod); + Ok(Self(global_id)) + } + + pub fn get_string_repr(&self) -> String { + todo!() + } + + pub fn generate_from_string(value: String) -> CustomResult { + let id = GlobalId::from_string(value) + .change_context(GlobalPaymentMethodIdError::ConstructionError)?; + Ok(Self(id)) + } +} + +impl diesel::Queryable for GlobalPaymentMethodId +where + DB: diesel::backend::Backend, + Self: diesel::deserialize::FromSql, +{ + type Row = Self; + fn build(row: Self::Row) -> diesel::deserialize::Result { + Ok(row) + } +} + +impl ToSql for GlobalPaymentMethodId +where + DB: Backend, + GlobalId: ToSql, +{ + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, DB>, + ) -> diesel::serialize::Result { + let id = self.get_global_id(); + id.to_sql(out) + } +} + +impl FromSql for GlobalPaymentMethodId +where + DB: Backend, + GlobalId: FromSql, +{ + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + let global_id = GlobalId::from_sql(value)?; + Ok(Self(global_id)) + } +} diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index ca919eb15d..3ad58e62dc 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -88,7 +88,7 @@ pub struct PaymentMethod { pub payment_method_billing_address: Option, pub updated_by: Option, pub locker_fingerprint_id: Option, - pub id: String, + pub id: common_utils::id_type::GlobalPaymentMethodId, pub version: common_enums::ApiVersion, pub network_token_requestor_reference_id: Option, pub network_token_locker_id: Option, @@ -105,7 +105,7 @@ impl PaymentMethod { } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] - pub fn get_id(&self) -> &String { + pub fn get_id(&self) -> &common_utils::id_type::GlobalPaymentMethodId { &self.id } } @@ -179,7 +179,7 @@ pub struct PaymentMethodNew { pub payment_method_billing_address: Option, pub updated_by: Option, pub locker_fingerprint_id: Option, - pub id: String, + pub id: common_utils::id_type::GlobalPaymentMethodId, pub version: common_enums::ApiVersion, pub network_token_requestor_reference_id: Option, pub network_token_locker_id: Option, @@ -200,7 +200,7 @@ impl PaymentMethodNew { } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] - pub fn get_id(&self) -> &String { + pub fn get_id(&self) -> &common_utils::id_type::GlobalPaymentMethodId { &self.id } } diff --git a/crates/diesel_models/src/query/payment_method.rs b/crates/diesel_models/src/query/payment_method.rs index 37c41c19dd..321c7a6829 100644 --- a/crates/diesel_models/src/query/payment_method.rs +++ b/crates/diesel_models/src/query/payment_method.rs @@ -211,7 +211,10 @@ impl PaymentMethod { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl PaymentMethod { - pub async fn find_by_id(conn: &PgPooledConn, id: &str) -> StorageResult { + pub async fn find_by_id( + conn: &PgPooledConn, + id: &common_utils::id_type::GlobalPaymentMethodId, + ) -> StorageResult { generics::generic_find_one::<::Table, _, _>(conn, pm_id.eq(id.to_owned())) .await } diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index e7cbae9e88..af3c657c34 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -77,7 +77,7 @@ pub struct PaymentMethod { pub payment_method_billing_address: OptionalEncryptableValue, pub updated_by: Option, pub locker_fingerprint_id: Option, - pub id: String, + pub id: common_utils::id_type::GlobalPaymentMethodId, pub version: common_enums::ApiVersion, pub network_token_requestor_reference_id: Option, pub network_token_locker_id: Option, @@ -94,7 +94,7 @@ impl PaymentMethod { } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] - pub fn get_id(&self) -> &String { + pub fn get_id(&self) -> &common_utils::id_type::GlobalPaymentMethodId { &self.id } } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 0c6e06cc42..bae6243d2e 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -37,7 +37,7 @@ v2 = ["customer_v2", "payment_methods_v2", "payment_v2", "common_default", "api_ v1 = ["common_default", "api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "storage_impl/v1", "hyperswitch_interfaces/v1", "kgraph_utils/v1"] customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2", "storage_impl/customer_v2"] payment_v2 = ["api_models/payment_v2", "diesel_models/payment_v2", "hyperswitch_domain_models/payment_v2", "storage_impl/payment_v2"] -payment_methods_v2 = ["api_models/payment_methods_v2", "diesel_models/payment_methods_v2", "hyperswitch_domain_models/payment_methods_v2", "storage_impl/payment_methods_v2"] +payment_methods_v2 = ["api_models/payment_methods_v2", "diesel_models/payment_methods_v2", "hyperswitch_domain_models/payment_methods_v2", "storage_impl/payment_methods_v2", "common_utils/payment_methods_v2"] # Partial Auth # The feature reduces the overhead of the router authenticating the merchant for every request, and trusts on `x-merchant-id` header to be present in the request. diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 3d4a29e0f3..1f4fd00bbc 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -135,3 +135,15 @@ pub const DEFAULT_UNIFIED_ERROR_MESSAGE: &str = "Something went wrong"; // Recon's feature tag pub const RECON_FEATURE_TAG: &str = "RECONCILIATION AND SETTLEMENT"; + +/// Vault Add request url +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub const ADD_VAULT_REQUEST_URL: &str = "/vault/add"; + +/// Vault Get Fingerprint request url +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/fingerprint"; + +/// Vault Header content type +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub const VAULT_HEADER_CONTENT_TYPE: &str = "application/json"; diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index d8a28ebdf2..c56bfaca91 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -148,6 +148,12 @@ pub enum VaultError { SavePaymentMethodFailed, #[error("Failed to generate fingerprint")] GenerateFingerprintFailed, + #[error("Failed to encrypt vault request")] + RequestEncryptionFailed, + #[error("Failed to decrypt vault response")] + ResponseDecryptionFailed, + #[error("Failed to call vault")] + VaultAPIError, #[error("Failed while calling locker API")] ApiError, } diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 72fbc3d3d4..fa7ddfce99 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -21,9 +21,16 @@ use api_models::payment_methods; pub use api_models::{enums::PayoutConnectors, payouts as payout_types}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use common_utils::ext_traits::Encode; -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use common_utils::ext_traits::OptionExt; use common_utils::{consts::DEFAULT_LOCALE, id_type::CustomerId}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use common_utils::{ + crypto::{self, Encryptable}, + ext_traits::{AsyncExt, Encode, ValueExt}, + fp_utils::when, + generate_id, id_type, + request::RequestContent, + types as util_types, +}; use diesel_models::{ enums, GenericLinkNew, PaymentMethodCollectLink, PaymentMethodCollectLinkData, }; @@ -31,7 +38,11 @@ use error_stack::{report, ResultExt}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData}; use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use masking::ExposeInterface; use masking::PeekInterface; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use masking::Secret; use router_env::{instrument, tracing}; use time::Duration; @@ -39,11 +50,25 @@ use super::{ errors::{RouterResponse, StorageErrorExt}, pm_auth, }; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::{ + configs::settings, + core::{payment_methods::transformers as pm_transforms, utils as core_utils}, + headers, logger, + routes::payment_methods as pm_routes, + services::encryption, + types::{ + api::{self, payment_methods::PaymentMethodCreateExt}, + payment_methods as pm_types, + storage::PaymentMethodListContext, + }, + utils::ext_traits::OptionExt, +}; use crate::{ consts, core::{ errors::{self, RouterResult}, - payments::helpers, + payments::helpers as payment_helpers, }, routes::{app::StorageInterface, SessionState}, services, @@ -67,7 +92,7 @@ pub async fn retrieve_payment_method( ) -> RouterResult<(Option, Option)> { match pm_data { pm_opt @ Some(pm @ domain::PaymentMethodData::Card(_)) => { - let payment_token = helpers::store_payment_method_data_in_vault( + let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, @@ -81,7 +106,7 @@ pub async fn retrieve_payment_method( Ok((pm_opt.to_owned(), payment_token)) } pm_opt @ Some(pm @ domain::PaymentMethodData::BankDebit(_)) => { - let payment_token = helpers::store_payment_method_data_in_vault( + let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, @@ -104,7 +129,7 @@ pub async fn retrieve_payment_method( pm @ Some(domain::PaymentMethodData::GiftCard(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::OpenBanking(_)) => Ok((pm.to_owned(), None)), pm_opt @ Some(pm @ domain::PaymentMethodData::BankTransfer(_)) => { - let payment_token = helpers::store_payment_method_data_in_vault( + let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, @@ -118,7 +143,7 @@ pub async fn retrieve_payment_method( Ok((pm_opt.to_owned(), payment_token)) } pm_opt @ Some(pm @ domain::PaymentMethodData::Wallet(_)) => { - let payment_token = helpers::store_payment_method_data_in_vault( + let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, @@ -132,7 +157,7 @@ pub async fn retrieve_payment_method( Ok((pm_opt.to_owned(), payment_token)) } pm_opt @ Some(pm @ domain::PaymentMethodData::BankRedirect(_)) => { - let payment_token = helpers::store_payment_method_data_in_vault( + let payment_token = payment_helpers::store_payment_method_data_in_vault( state, payment_attempt, payment_intent, @@ -414,6 +439,10 @@ fn generate_task_id_for_payment_method_status_update_workflow( format!("{runner}_{task}_{key_id}") } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn add_payment_method_status_update_task( db: &dyn StorageInterface, payment_method: &domain::PaymentMethod, @@ -483,7 +512,10 @@ pub async fn retrieve_payment_method_with_token( todo!() } -#[cfg(feature = "v1")] +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn retrieve_payment_method_with_token( @@ -500,7 +532,7 @@ pub async fn retrieve_payment_method_with_token( ) -> RouterResult { let token = match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { - helpers::retrieve_payment_method_with_temporary_token( + payment_helpers::retrieve_payment_method_with_temporary_token( state, &generic_token.token, payment_intent, @@ -519,7 +551,7 @@ pub async fn retrieve_payment_method_with_token( } storage::PaymentTokenData::Temporary(generic_token) => { - helpers::retrieve_payment_method_with_temporary_token( + payment_helpers::retrieve_payment_method_with_temporary_token( state, &generic_token.token, payment_intent, @@ -538,7 +570,7 @@ pub async fn retrieve_payment_method_with_token( } storage::PaymentTokenData::Permanent(card_token) => { - helpers::retrieve_card_with_permanent_token( + payment_helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), card_token @@ -572,7 +604,7 @@ pub async fn retrieve_payment_method_with_token( } storage::PaymentTokenData::PermanentCard(card_token) => { - helpers::retrieve_card_with_permanent_token( + payment_helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), card_token @@ -793,3 +825,950 @@ pub(crate) async fn get_payment_method_create_request( .attach_printable("PaymentMethodData required Or Card is already saved")), } } + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn create_payment_method( + state: &SessionState, + req: api::PaymentMethodCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> errors::RouterResponse { + req.validate()?; + + let db = &*state.store; + let merchant_id = merchant_account.get_id(); + let customer_id = req.customer_id.to_owned(); + + db.find_customer_by_merchant_reference_id_merchant_id( + &(state.into()), + &customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let payment_method_billing_address: Option>> = req + .billing + .clone() + .async_map(|billing| cards::create_encrypted_data(state, key_store, billing)) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method billing address")?; + + // create pm + let payment_method_id = + common_utils::id_type::GlobalPaymentMethodId::generate("random_cell_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to generate GlobalPaymentMethodId")?; + + let payment_method = create_payment_method_for_intent( + state, + req.metadata.clone(), + &customer_id, + payment_method_id, + merchant_id, + key_store, + merchant_account.storage_scheme, + payment_method_billing_address.map(Into::into), + ) + .await + .attach_printable("Failed to add Payment method to DB")?; + + let vaulting_result = + vault_payment_method(state, &req.payment_method_data, merchant_account, key_store).await; + + let response = match vaulting_result { + Ok(resp) => { + let pm_update = create_pm_additional_data_update( + &req.payment_method_data, + state, + key_store, + Some(resp.vault_id), + Some(req.payment_method), + Some(req.payment_method_type), + ) + .await + .attach_printable("Unable to create Payment method data")?; + + let payment_method = db + .update_payment_method( + &(state.into()), + &key_store, + payment_method, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + + let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + + Ok(resp) + } + Err(e) => { + let pm_update = storage::PaymentMethodUpdate::StatusUpdate { + status: Some(enums::PaymentMethodStatus::Inactive), + }; + + db.update_payment_method( + &(state.into()), + &key_store, + payment_method, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + + Err(e) + } + }?; + + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn payment_method_intent_create( + state: &SessionState, + req: api::PaymentMethodIntentCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> errors::RouterResponse { + let db = &*state.store; + let merchant_id = merchant_account.get_id(); + let customer_id = req.customer_id.to_owned(); + + db.find_customer_by_merchant_reference_id_merchant_id( + &(state.into()), + &customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let payment_method_billing_address: Option>> = req + .billing + .clone() + .async_map(|billing| cards::create_encrypted_data(state, key_store, billing)) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method billing address")?; + + // create pm entry + + let payment_method_id = + common_utils::id_type::GlobalPaymentMethodId::generate("random_cell_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to generate GlobalPaymentMethodId")?; + + let payment_method = create_payment_method_for_intent( + state, + req.metadata.clone(), + &customer_id, + payment_method_id, + merchant_id, + key_store, + merchant_account.storage_scheme, + payment_method_billing_address.map(Into::into), + ) + .await + .attach_printable("Failed to add Payment method to DB")?; + + let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + + Ok(services::ApplicationResponse::Json(resp)) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn payment_method_intent_confirm( + state: &SessionState, + req: api::PaymentMethodIntentConfirm, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + pm_id: String, +) -> errors::RouterResponse { + req.validate()?; + + let db = &*state.store; + let client_secret = req.client_secret.clone(); + let pm_id = common_utils::id_type::GlobalPaymentMethodId::generate_from_string(pm_id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to generate GlobalPaymentMethodId")?; + + let payment_method = db + .find_payment_method( + &(state.into()), + &key_store, + &pm_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?; + + when( + cards::authenticate_pm_client_secret_and_check_expiry(&client_secret, &payment_method)?, + || Err(errors::ApiErrorResponse::ClientSecretExpired), + )?; + + when( + payment_method.status != enums::PaymentMethodStatus::AwaitingData, + || { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid pm_id provided: This Payment method cannot be confirmed" + .to_string(), + }) + }, + )?; + + let customer_id = payment_method.customer_id.to_owned(); + db.find_customer_by_merchant_reference_id_merchant_id( + &(state.into()), + &customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let vaulting_result = + vault_payment_method(state, &req.payment_method_data, merchant_account, key_store).await; + + let response = match vaulting_result { + Ok(resp) => { + let pm_update = create_pm_additional_data_update( + &req.payment_method_data, + state, + key_store, + Some(resp.vault_id), + Some(req.payment_method), + Some(req.payment_method_type), + ) + .await + .attach_printable("Unable to create Payment method data")?; + + let payment_method = db + .update_payment_method( + &(state.into()), + &key_store, + payment_method, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + + let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + + Ok(resp) + } + Err(e) => { + let pm_update = storage::PaymentMethodUpdate::StatusUpdate { + status: Some(enums::PaymentMethodStatus::Inactive), + }; + + db.update_payment_method( + &(state.into()), + &key_store, + payment_method, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment method in db")?; + + Err(e) + } + }?; + + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(all( + feature = "v2", + feature = "payment_methods_v2", + feature = "customer_v2" +))] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn create_payment_method_in_db( + state: &SessionState, + req: &api::PaymentMethodCreate, + customer_id: &id_type::CustomerId, + payment_method_id: id_type::GlobalPaymentMethodId, + locker_id: Option, + merchant_id: &id_type::MerchantId, + pm_metadata: Option, + customer_acceptance: Option, + payment_method_data: crypto::OptionalEncryptableValue, + key_store: &domain::MerchantKeyStore, + connector_mandate_details: Option, + status: Option, + network_transaction_id: Option, + storage_scheme: enums::MerchantStorageScheme, + payment_method_billing_address: crypto::OptionalEncryptableValue, + card_scheme: Option, +) -> errors::CustomResult { + let db = &*state.store; + let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); + let current_time = common_utils::date_time::now(); + + let response = db + .insert_payment_method( + &state.into(), + key_store, + domain::PaymentMethod { + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_owned(), + id: payment_method_id, + locker_id, + payment_method: Some(req.payment_method), + payment_method_type: Some(req.payment_method_type), + metadata: pm_metadata, + payment_method_data, + connector_mandate_details, + customer_acceptance, + client_secret: Some(client_secret), + status: status.unwrap_or(enums::PaymentMethodStatus::Active), + network_transaction_id: network_transaction_id.to_owned(), + created_at: current_time, + last_modified: current_time, + last_used_at: current_time, + payment_method_billing_address, + updated_by: None, + version: domain::consts::API_VERSION, + locker_fingerprint_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, + network_token_requestor_reference_id: None, + }, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?; + + Ok(response) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn create_payment_method_for_intent( + state: &SessionState, + metadata: Option, + customer_id: &id_type::CustomerId, + payment_method_id: id_type::GlobalPaymentMethodId, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + payment_method_billing_address: crypto::OptionalEncryptableValue, +) -> errors::CustomResult { + let db = &*state.store; + let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); + let current_time = common_utils::date_time::now(); + + let response = db + .insert_payment_method( + &state.into(), + key_store, + domain::PaymentMethod { + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_owned(), + id: payment_method_id, + locker_id: None, + payment_method: None, + payment_method_type: None, + metadata, + payment_method_data: None, + connector_mandate_details: None, + customer_acceptance: None, + client_secret: Some(client_secret), + status: enums::PaymentMethodStatus::AwaitingData, + network_transaction_id: None, + created_at: current_time, + last_modified: current_time, + last_used_at: current_time, + payment_method_billing_address, + updated_by: None, + version: domain::consts::API_VERSION, + locker_fingerprint_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, + network_token_requestor_reference_id: None, + }, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?; + + Ok(response) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn create_pm_additional_data_update( + pmd: &api::PaymentMethodCreateData, + state: &SessionState, + key_store: &domain::MerchantKeyStore, + vault_id: Option, + payment_method: Option, + payment_method_type: Option, +) -> errors::RouterResult { + let card = match pmd.clone() { + api::PaymentMethodCreateData::Card(card) => api::PaymentMethodsData::Card(card.into()), + }; + + let pmd: Encryptable> = + cards::create_encrypted_data(state, key_store, card) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method data")?; + + let pm_update = storage::PaymentMethodUpdate::AdditionalDataUpdate { + status: Some(enums::PaymentMethodStatus::Active), + locker_id: vault_id, + payment_method, + payment_method_type, + payment_method_data: Some(pmd).map(Into::into), + network_token_requestor_reference_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, + }; + + Ok(pm_update) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn vault_payment_method( + state: &SessionState, + pmd: &api::PaymentMethodCreateData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> errors::RouterResult { + let db = &*state.store; + + // get fingerprint_id from locker + let fingerprint_id_from_locker = cards::get_fingerprint_id_from_locker(state, pmd) + .await + .attach_printable("Failed to get fingerprint_id from vault")?; + + // throw back error if payment method is duplicated + when( + Some( + db.find_payment_method_by_fingerprint_id( + &(state.into()), + key_store, + &fingerprint_id_from_locker, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to find payment method by fingerprint_id")?, + ) + .is_some(), + || { + Err(report!(errors::ApiErrorResponse::DuplicatePaymentMethod) + .attach_printable("Cannot vault duplicate payment method")) + }, + )?; + + let resp_from_locker = + cards::vault_payment_method_in_locker(state, merchant_account, pmd).await?; + + Ok(resp_from_locker) +} + +#[cfg(all( + feature = "v2", + feature = "payment_methods_v2", + feature = "customer_v2" +))] +async fn get_pm_list_context( + state: &SessionState, + payment_method: &enums::PaymentMethod, + _key_store: &domain::MerchantKeyStore, + pm: &domain::PaymentMethod, + _parent_payment_method_token: Option, + is_payment_associated: bool, +) -> Result, error_stack::Report> { + let payment_method_retrieval_context = match payment_method { + enums::PaymentMethod::Card => { + let card_details = cards::get_card_details_with_locker_fallback(pm, state).await?; + + card_details.as_ref().map(|card| PaymentMethodListContext { + card_details: Some(card.clone()), + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: is_payment_associated.then_some( + storage::PaymentTokenData::permanent_card( + Some(pm.get_id().clone()), + pm.locker_id.clone().or(Some(pm.get_id().get_string_repr())), + pm.locker_id + .clone() + .unwrap_or(pm.get_id().get_string_repr()), + ), + ), + }) + } + + enums::PaymentMethod::BankDebit => { + // Retrieve the pm_auth connector details so that it can be tokenized + let bank_account_token_data = cards::get_bank_account_connector_details(pm) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + + bank_account_token_data.map(|data| { + let token_data = storage::PaymentTokenData::AuthBankDebit(data); + + PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: is_payment_associated.then_some(token_data), + } + }) + } + + _ => Some(PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: is_payment_associated.then_some( + storage::PaymentTokenData::temporary_generic(generate_id( + consts::ID_LENGTH, + "token", + )), + ), + }), + }; + + Ok(payment_method_retrieval_context) +} + +#[cfg(all( + feature = "v2", + feature = "payment_methods_v2", + feature = "customer_v2" +))] +pub async fn list_customer_payment_method_util( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: Option, + customer_id: Option, + is_payment_associated: bool, +) -> errors::RouterResponse { + let limit = req.as_ref().and_then(|pml_req| pml_req.limit); + + let (customer_id, payment_intent) = if is_payment_associated { + let cloned_secret = req.and_then(|r| r.client_secret.clone()); + let payment_intent = payment_helpers::verify_payment_intent_time_and_client_secret( + &state, + &merchant_account, + &key_store, + cloned_secret, + ) + .await?; + + ( + payment_intent + .as_ref() + .and_then(|pi| pi.customer_id.clone()), + payment_intent, + ) + } else { + (customer_id, None) + }; + + let resp = if let Some(cust) = customer_id { + Box::pin(list_customer_payment_method( + &state, + merchant_account, + key_store, + payment_intent, + &cust, + limit, + is_payment_associated, + )) + .await? + } else { + let response = api::CustomerPaymentMethodsListResponse { + customer_payment_methods: Vec::new(), + is_guest_customer: Some(true), + }; + services::ApplicationResponse::Json(response) + }; + + Ok(resp) +} + +#[cfg(all( + feature = "v2", + feature = "payment_methods_v2", + feature = "customer_v2" +))] +pub async fn list_customer_payment_method( + state: &SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payment_intent: Option, + customer_id: &id_type::CustomerId, + limit: Option, + is_payment_associated: bool, +) -> errors::RouterResponse { + let db = &*state.store; + let key_manager_state = &(state).into(); + // let key = key_store.key.get_inner().peek(); + + let customer = db + .find_customer_by_merchant_reference_id_merchant_id( + key_manager_state, + customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let payments_info = payment_intent + .async_map(|pi| { + pm_types::SavedPMLPaymentsInfo::form_payments_info( + pi, + &merchant_account, + db, + key_manager_state, + &key_store, + ) + }) + .await + .transpose()?; + + let saved_payment_methods = db + .find_payment_method_by_customer_id_merchant_id_status( + key_manager_state, + &key_store, + customer_id, + merchant_account.get_id(), + common_enums::PaymentMethodStatus::Active, + limit, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let mut filtered_saved_payment_methods_ctx = Vec::new(); + for pm in saved_payment_methods.into_iter() { + let payment_method = pm.payment_method.get_required_value("payment_method")?; + let parent_payment_method_token = + is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token")); + + let pm_list_context = get_pm_list_context( + state, + &payment_method, + &key_store, + &pm, + parent_payment_method_token.clone(), + is_payment_associated, + ) + .await?; + + if let Some(ctx) = pm_list_context { + filtered_saved_payment_methods_ctx.push((ctx, parent_payment_method_token, pm)); + } + } + + let pm_list_futures = filtered_saved_payment_methods_ctx + .into_iter() + .map(|ctx| { + generate_saved_pm_response( + state, + &key_store, + &merchant_account, + ctx, + &customer, + payments_info.as_ref(), + ) + }) + .collect::>(); + + let final_result = futures::future::join_all(pm_list_futures).await; + + let mut customer_pms = Vec::new(); + for result in final_result.into_iter() { + let pma = result.attach_printable("saved pm list failed")?; + customer_pms.push(pma); + } + + let mut response = api::CustomerPaymentMethodsListResponse { + customer_payment_methods: customer_pms, + is_guest_customer: is_payment_associated.then_some(false), //to return this key only when the request is tied to a payment intent + }; + + if is_payment_associated { + Box::pin(cards::perform_surcharge_ops( + payments_info.as_ref().map(|pi| pi.payment_intent.clone()), + state, + merchant_account, + key_store, + payments_info.and_then(|pi| pi.business_profile), + &mut response, + )) + .await?; + } + + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +async fn generate_saved_pm_response( + state: &SessionState, + key_store: &domain::MerchantKeyStore, + merchant_account: &domain::MerchantAccount, + pm_list_context: ( + PaymentMethodListContext, + Option, + domain::PaymentMethod, + ), + customer: &domain::Customer, + payment_info: Option<&pm_types::SavedPMLPaymentsInfo>, +) -> Result> { + let (pm_list_context, parent_payment_method_token, pm) = pm_list_context; + let payment_method = pm.payment_method.get_required_value("payment_method")?; + + let bank_details = if payment_method == enums::PaymentMethod::BankDebit { + cards::get_masked_bank_details(&pm) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) + } else { + None + }; + + let payment_method_billing = pm + .payment_method_billing_address + .clone() + .map(|decrypted_data| decrypted_data.into_inner().expose()) + .map(|decrypted_value| decrypted_value.parse_value("payment_method_billing_address")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse payment method billing address details")?; + + let connector_mandate_details = pm + .connector_mandate_details + .clone() + .map(|val| val.parse_value::("PaymentsMandateReference")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; + + let (is_connector_agnostic_mit_enabled, requires_cvv, off_session_payment_flag, profile_id) = + payment_info + .map(|pi| { + ( + pi.is_connector_agnostic_mit_enabled, + pi.requires_cvv, + pi.off_session_payment_flag, + pi.business_profile + .as_ref() + .map(|profile| profile.get_id().to_owned()), + ) + }) + .unwrap_or((false, false, false, Default::default())); + + let mca_enabled = cards::get_mca_status( + state, + key_store, + profile_id, + merchant_account.get_id(), + is_connector_agnostic_mit_enabled, + connector_mandate_details, + pm.network_transaction_id.as_ref(), + ) + .await?; + + let requires_cvv = if is_connector_agnostic_mit_enabled { + requires_cvv + && !(off_session_payment_flag + && (pm.connector_mandate_details.is_some() || pm.network_transaction_id.is_some())) + } else { + requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()) + }; + + let pmd = if let Some(card) = pm_list_context.card_details.as_ref() { + Some(api::PaymentMethodListData::Card(card.clone())) + } else if cfg!(feature = "payouts") { + pm_list_context + .bank_transfer_details + .clone() + .map(api::PaymentMethodListData::Bank) + } else { + None + }; + + let pma = api::CustomerPaymentMethod { + payment_token: parent_payment_method_token.clone(), + payment_method_id: pm.get_id().get_string_repr(), + customer_id: pm.customer_id.to_owned(), + payment_method, + payment_method_type: pm.payment_method_type, + payment_method_data: pmd, + metadata: pm.metadata.clone(), + recurring_enabled: mca_enabled, + created: Some(pm.created_at), + bank: bank_details, + surcharge_details: None, + requires_cvv: requires_cvv + && !(off_session_payment_flag && pm.connector_mandate_details.is_some()), + last_used_at: Some(pm.last_used_at), + is_default: customer.default_payment_method_id.is_some() + && customer.default_payment_method_id.as_ref() == Some(&pm.get_id().get_string_repr()), + billing: payment_method_billing, + }; + + payment_info + .async_map(|pi| { + pi.perform_payment_ops(state, parent_payment_method_token, &pma, pm_list_context) + }) + .await + .transpose()?; + + Ok(pma) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl pm_types::SavedPMLPaymentsInfo { + pub async fn form_payments_info( + payment_intent: storage::PaymentIntent, + merchant_account: &domain::MerchantAccount, + db: &dyn StorageInterface, + key_manager_state: &util_types::keymanager::KeyManagerState, + key_store: &domain::MerchantKeyStore, + ) -> errors::RouterResult { + let requires_cvv = db + .find_config_by_key_unwrap_or( + format!( + "{}_requires_cvv", + merchant_account.get_id().get_string_repr() + ) + .as_str(), + Some("true".to_string()), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch requires_cvv config")? + .config + != "false"; + + let off_session_payment_flag = matches!( + payment_intent.setup_future_usage, + Some(common_enums::FutureUsage::OffSession) + ); + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")? + .clone(); + + let business_profile = core_utils::validate_and_get_business_profile( + db, + key_manager_state, + key_store, + Some(profile_id).as_ref(), + merchant_account.get_id(), + ) + .await?; + + let is_connector_agnostic_mit_enabled = business_profile + .as_ref() + .and_then(|business_profile| business_profile.is_connector_agnostic_mit_enabled) + .unwrap_or(false); + + Ok(Self { + payment_intent, + business_profile, + requires_cvv, + off_session_payment_flag, + is_connector_agnostic_mit_enabled, + }) + } + + pub async fn perform_payment_ops( + &self, + state: &SessionState, + parent_payment_method_token: Option, + pma: &api::CustomerPaymentMethod, + pm_list_context: PaymentMethodListContext, + ) -> errors::RouterResult<()> { + let token = parent_payment_method_token + .as_ref() + .get_required_value("parent_payment_method_token")?; + let hyperswitch_token_data = pm_list_context + .hyperswitch_token_data + .get_required_value("PaymentTokenData")?; + + let intent_fulfillment_time = self + .business_profile + .as_ref() + .and_then(|b_profile| b_profile.get_order_fulfillment_time()) + .unwrap_or(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME); + + pm_routes::ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method)) + .insert(intent_fulfillment_time, hyperswitch_token_data, state) + .await?; + + if let Some(metadata) = pma.metadata.clone() { + let pm_metadata_vec: pm_transforms::PaymentMethodMetadata = metadata + .parse_value("PaymentMethodMetadata") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to deserialize metadata to PaymentmethodMetadata struct", + )?; + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + for pm_metadata in pm_metadata_vec.payment_method_tokenization { + let key = format!( + "pm_token_{}_{}_{}", + token, pma.payment_method, pm_metadata.0 + ); + + redis_conn + .set_key_with_expiry(&key, pm_metadata.1, intent_fulfillment_time) + .await + .change_context(errors::StorageError::KVError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add data in redis")?; + } + } + + Ok(()) + } +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 22350f77e6..79d1226f13 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -69,8 +69,6 @@ use super::surcharge_decision_configs::{ use crate::routes::app::SessionStateInfo; #[cfg(feature = "payouts")] use crate::types::domain::types::AsyncLift; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use crate::utils::{self}; use crate::{ configs::{ defaults::{get_billing_required_fields, get_shipping_required_fields}, @@ -95,8 +93,14 @@ use crate::{ storage::{self, enums, PaymentMethodListContext, PaymentTokenData}, transformers::ForeignTryFrom, }, + utils, utils::OptionExt, }; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::{ + consts as router_consts, core::payment_methods as pm_core, headers, + types::payment_methods as pm_types, utils::ConnectorResponseExt, +}; #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -217,32 +221,35 @@ pub async fn create_payment_method( Ok(response) } -#[cfg(all( - feature = "v2", - feature = "payment_methods_v2", - feature = "customer_v2" -))] -#[instrument(skip_all)] -#[allow(clippy::too_many_arguments)] -pub async fn create_payment_method( - _state: &routes::SessionState, - _req: &api::PaymentMethodCreate, - _customer_id: &id_type::CustomerId, - _payment_method_id: &str, - _locker_id: Option, - _merchant_id: &id_type::MerchantId, - _pm_metadata: Option, - _customer_acceptance: Option, - _payment_method_data: Option, - _key_store: &domain::MerchantKeyStore, - _connector_mandate_details: Option, - _status: Option, - _network_transaction_id: Option, - _storage_scheme: MerchantStorageScheme, - _payment_method_billing_address: Option, - _card_scheme: Option, -) -> errors::CustomResult { - todo!() +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +async fn create_vault_request( + jwekey: &settings::Jwekey, + locker: &settings::Locker, + payload: Vec, +) -> errors::CustomResult { + let private_key = jwekey.vault_private_key.peek().as_bytes(); + + let jws = services::encryption::jws_sign_payload( + &payload, + &locker.locker_signing_key_id, + private_key, + ) + .await + .change_context(errors::VaultError::RequestEncryptionFailed)?; + + let jwe_payload = payment_methods::create_jwe_body_for_vault(jwekey, &jws).await?; + + let mut url = locker.host.to_owned(); + url.push_str(R::get_vaulting_request_url()); + let mut request = services::Request::new(services::Method::Post, &url); + request.add_header( + headers::CONTENT_TYPE, + router_consts::VAULT_HEADER_CONTENT_TYPE.into(), + ); + request.set_body(common_utils::request::RequestContent::Json(Box::new( + jwe_payload, + ))); + Ok(request) } #[cfg(all( @@ -911,17 +918,6 @@ pub async fn get_client_secret_or_add_payment_method( } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[instrument(skip_all)] -pub async fn get_client_secret_or_add_payment_method( - _state: &routes::SessionState, - _req: api::PaymentMethodCreate, - _merchant_account: &domain::MerchantAccount, - _key_store: &domain::MerchantKeyStore, -) -> errors::RouterResponse { - todo!() -} - #[instrument(skip_all)] pub fn authenticate_pm_client_secret_and_check_expiry( req_client_secret: &String, @@ -1417,13 +1413,64 @@ pub async fn add_payment_method( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] -pub async fn add_payment_method( - _state: &routes::SessionState, - _req: api::PaymentMethodCreate, - _merchant_account: &domain::MerchantAccount, - _key_store: &domain::MerchantKeyStore, -) -> errors::RouterResponse { - todo!() +pub async fn get_fingerprint_id_from_locker< + D: pm_types::VaultingDataInterface + serde::Serialize, +>( + state: &routes::SessionState, + data: &D, +) -> errors::RouterResult { + let key = data.get_vaulting_data_key(); + let data = serde_json::to_value(data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode Vaulting data to value")? + .to_string(); + + let payload = pm_types::VaultFingerprintRequest { key, data } + .encode_to_vec() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode VaultFingerprintRequest")?; + + let resp = call_to_vault::(state, payload) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get response from locker")?; + + let fingerprint_resp: pm_types::VaultFingerprintResponse = resp + .parse_struct("VaultFingerprintResp") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse data into VaultFingerprintResp")?; + + Ok(fingerprint_resp.fingerprint_id) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn vault_payment_method_in_locker( + state: &routes::SessionState, + merchant_account: &domain::MerchantAccount, + pmd: &api::PaymentMethodCreateData, +) -> errors::RouterResult { + let payload = pm_types::AddVaultRequest { + entity_id: merchant_account.get_id().to_owned(), + vault_id: uuid::Uuid::now_v7().to_string(), + data: pmd.clone(), + ttl: state.conf.locker.ttl_for_storage_in_secs, + } + .encode_to_vec() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode AddVaultRequest")?; + + let resp = call_to_vault::(state, payload) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get response from locker")?; + + let stored_pm_resp: pm_types::AddVaultResponse = resp + .parse_struct("AddVaultResponse") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse data into AddVaultResponse")?; + + Ok(stored_pm_resp) } #[cfg(all( @@ -1506,40 +1553,7 @@ pub async fn insert_payment_method( storage_scheme: MerchantStorageScheme, payment_method_billing_address: Option, ) -> errors::RouterResult { - let pm_card_details = match &resp.payment_method_data { - Some(api::PaymentMethodResponseData::Card(card_data)) => Some(PaymentMethodsData::Card( - CardDetailsPaymentMethod::from(card_data.clone()), - )), - _ => None, - }; - - let pm_data_encrypted: Option>> = pm_card_details - .clone() - .async_map(|pm_card| create_encrypted_data(state, key_store, pm_card)) - .await - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt payment method data")?; - - create_payment_method( - state, - req, - customer_id, - &resp.payment_method_id, - locker_id, - merchant_id, - pm_metadata, - customer_acceptance, - pm_data_encrypted.map(Into::into), - key_store, - connector_mandate_details, - None, - network_transaction_id, - storage_scheme, - payment_method_billing_address, - None, - ) - .await + todo!() } #[cfg(all( @@ -2225,6 +2239,37 @@ pub async fn add_card_to_hs_locker( Ok(stored_card) } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn call_to_vault( + state: &routes::SessionState, + payload: Vec, +) -> errors::CustomResult { + let locker = &state.conf.locker; + let jwekey = state.conf.jwekey.get_inner(); + + let request = create_vault_request::(jwekey, locker, payload).await?; + let response = services::call_connector_api(state, request, "vault_in_locker") + .await + .change_context(errors::VaultError::VaultAPIError); + + let jwe_body: services::JweBody = response + .get_response_inner("JweBody") + .change_context(errors::VaultError::ResponseDeserializationFailed) + .attach_printable("Failed to get JweBody from vault response")?; + + let decrypted_payload = payment_methods::get_decrypted_vault_response_payload( + jwekey, + jwe_body, + locker.decryption_scheme.clone(), + ) + .await + .change_context(errors::VaultError::ResponseDecryptionFailed) + .attach_printable("Error getting decrypted vault response payload")?; + + Ok(decrypted_payload) +} + #[instrument(skip_all)] pub async fn call_locker_api( state: &routes::SessionState, @@ -3819,6 +3864,10 @@ fn should_collect_shipping_or_billing_details_from_wallet_connector( } } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2"), +))] async fn validate_payment_method_and_client_secret( state: &routes::SessionState, cs: &String, @@ -4235,63 +4284,6 @@ fn filter_recurring_based( recurring_enabled.map_or(true, |enabled| payment_method.recurring_enabled == enabled) } -#[cfg(all( - feature = "v2", - feature = "payment_methods_v2", - feature = "customer_v2" -))] -pub async fn list_customer_payment_method_util( - state: routes::SessionState, - merchant_account: domain::MerchantAccount, - key_store: domain::MerchantKeyStore, - req: Option, - customer_id: Option, - is_payment_associated: bool, -) -> errors::RouterResponse { - let limit = req.as_ref().and_then(|pml_req| pml_req.limit); - - let (customer_id, payment_intent) = if is_payment_associated { - let cloned_secret = req.and_then(|r| r.client_secret.clone()); - let payment_intent = helpers::verify_payment_intent_time_and_client_secret( - &state, - &merchant_account, - &key_store, - cloned_secret, - ) - .await?; - - ( - payment_intent - .as_ref() - .and_then(|pi| pi.customer_id.clone()), - payment_intent, - ) - } else { - (customer_id, None) - }; - - let resp = if let Some(cust) = customer_id { - Box::pin(list_customer_payment_method( - &state, - merchant_account, - key_store, - payment_intent, - &cust, - limit, - is_payment_associated, - )) - .await? - } else { - let response = api::CustomerPaymentMethodsListResponse { - customer_payment_methods: Vec::new(), - is_guest_customer: Some(true), - }; - services::ApplicationResponse::Json(response) - }; - - Ok(resp) -} - #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2"), @@ -4615,6 +4607,11 @@ pub async fn list_customer_payment_method( Ok(services::ApplicationResponse::Json(response)) } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2"), + not(feature = "customer_v2") +))] async fn get_pm_list_context( state: &routes::SessionState, payment_method: &enums::PaymentMethod, @@ -4706,7 +4703,11 @@ async fn get_pm_list_context( Ok(payment_method_retrieval_context) } -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "payment_v2")))] +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_v2"), + not(feature = "payment_methods_v2") +))] async fn perform_surcharge_ops( payment_intent: Option, state: &routes::SessionState, @@ -4751,8 +4752,8 @@ async fn perform_surcharge_ops( Ok(()) } -#[cfg(all(feature = "v2", feature = "payment_v2"))] -async fn perform_surcharge_ops( +#[cfg(all(feature = "v2", feature = "payment_v2", feature = "payment_methods_v2"))] +pub async fn perform_surcharge_ops( _payment_intent: Option, _state: &routes::SessionState, _merchant_account: domain::MerchantAccount, @@ -4763,370 +4764,6 @@ async fn perform_surcharge_ops( todo!() } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -struct SavedPMLPaymentsInfo { - pub payment_intent: storage::PaymentIntent, - pub business_profile: Option, - pub requires_cvv: bool, - pub off_session_payment_flag: bool, - pub is_connector_agnostic_mit_enabled: bool, -} - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl SavedPMLPaymentsInfo { - pub async fn form_payments_info( - payment_intent: storage::PaymentIntent, - merchant_account: &domain::MerchantAccount, - db: &dyn db::StorageInterface, - key_manager_state: &KeyManagerState, - key_store: &domain::MerchantKeyStore, - ) -> errors::RouterResult { - let requires_cvv = db - .find_config_by_key_unwrap_or( - format!( - "{}_requires_cvv", - merchant_account.get_id().get_string_repr() - ) - .as_str(), - Some("true".to_string()), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch requires_cvv config")? - .config - != "false"; - - let off_session_payment_flag = matches!( - payment_intent.setup_future_usage, - Some(common_enums::FutureUsage::OffSession) - ); - - let profile_id = payment_intent - .profile_id - .as_ref() - .get_required_value("profile_id") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")? - .clone(); - - let business_profile = core_utils::validate_and_get_business_profile( - db, - key_manager_state, - key_store, - Some(profile_id).as_ref(), - merchant_account.get_id(), - ) - .await?; - - let is_connector_agnostic_mit_enabled = business_profile - .as_ref() - .and_then(|business_profile| business_profile.is_connector_agnostic_mit_enabled) - .unwrap_or(false); - - Ok(Self { - payment_intent, - business_profile, - requires_cvv, - off_session_payment_flag, - is_connector_agnostic_mit_enabled, - }) - } - - pub async fn perform_payment_ops( - &self, - state: &routes::SessionState, - parent_payment_method_token: Option, - pma: &api::CustomerPaymentMethod, - pm_list_context: PaymentMethodListContext, - ) -> errors::RouterResult<()> { - let token = parent_payment_method_token - .as_ref() - .get_required_value("parent_payment_method_token")?; - let hyperswitch_token_data = pm_list_context - .hyperswitch_token_data - .get_required_value("PaymentTokenData")?; - - let intent_fulfillment_time = self - .business_profile - .as_ref() - .and_then(|b_profile| b_profile.get_order_fulfillment_time()) - .unwrap_or(consts::DEFAULT_INTENT_FULFILLMENT_TIME); - - ParentPaymentMethodToken::create_key_for_token((token, pma.payment_method)) - .insert(intent_fulfillment_time, hyperswitch_token_data, state) - .await?; - - if let Some(metadata) = pma.metadata.clone() { - let pm_metadata_vec: payment_methods::PaymentMethodMetadata = metadata - .parse_value("PaymentMethodMetadata") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Failed to deserialize metadata to PaymentmethodMetadata struct", - )?; - - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - - for pm_metadata in pm_metadata_vec.payment_method_tokenization { - let key = format!( - "pm_token_{}_{}_{}", - token, pma.payment_method, pm_metadata.0 - ); - - redis_conn - .set_key_with_expiry(&key, pm_metadata.1, intent_fulfillment_time) - .await - .change_context(errors::StorageError::KVError) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to add data in redis")?; - } - } - - Ok(()) - } -} - -#[cfg(all( - feature = "v2", - feature = "payment_methods_v2", - feature = "customer_v2" -))] -pub async fn list_customer_payment_method( - state: &routes::SessionState, - merchant_account: domain::MerchantAccount, - key_store: domain::MerchantKeyStore, - payment_intent: Option, - customer_id: &id_type::CustomerId, - limit: Option, - is_payment_associated: bool, -) -> errors::RouterResponse { - let db = &*state.store; - let key_manager_state = &(state).into(); - // let key = key_store.key.get_inner().peek(); - - let customer = db - .find_customer_by_merchant_reference_id_merchant_id( - key_manager_state, - customer_id, - merchant_account.get_id(), - &key_store, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - - let payments_info = payment_intent - .async_map(|pi| { - SavedPMLPaymentsInfo::form_payments_info( - pi, - &merchant_account, - db, - key_manager_state, - &key_store, - ) - }) - .await - .transpose()?; - - let saved_payment_methods = db - .find_payment_method_by_customer_id_merchant_id_status( - key_manager_state, - &key_store, - customer_id, - merchant_account.get_id(), - common_enums::PaymentMethodStatus::Active, - limit, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; - - let mut filtered_saved_payment_methods_ctx = Vec::new(); - for pm in saved_payment_methods.into_iter() { - logger::debug!( - "Fetching payment method from locker for payment_method_id: {}", - pm.id - ); - let payment_method = pm.payment_method.get_required_value("payment_method")?; - let parent_payment_method_token = - is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token")); - - let pm_list_context = get_pm_list_context( - state, - &payment_method, - &key_store, - &pm, - parent_payment_method_token.clone(), - is_payment_associated, - ) - .await?; - - if let Some(ctx) = pm_list_context { - filtered_saved_payment_methods_ctx.push((ctx, parent_payment_method_token, pm)); - } - } - - let pm_list_futures = filtered_saved_payment_methods_ctx - .into_iter() - .map(|ctx| { - generate_saved_pm_response( - state, - &key_store, - &merchant_account, - ctx, - &customer, - payments_info.as_ref(), - ) - }) - .collect::>(); - - let final_result = futures::future::join_all(pm_list_futures).await; - - let mut customer_pms = Vec::new(); - for result in final_result.into_iter() { - let pma = result.attach_printable("saved pm list failed")?; - customer_pms.push(pma); - } - - let mut response = api::CustomerPaymentMethodsListResponse { - customer_payment_methods: customer_pms, - is_guest_customer: is_payment_associated.then_some(false), //to return this key only when the request is tied to a payment intent - }; - - if is_payment_associated { - Box::pin(perform_surcharge_ops( - payments_info.as_ref().map(|pi| pi.payment_intent.clone()), - state, - merchant_account, - key_store, - payments_info.and_then(|pi| pi.business_profile), - &mut response, - )) - .await?; - } - - Ok(services::ApplicationResponse::Json(response)) -} - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -async fn generate_saved_pm_response( - state: &routes::SessionState, - key_store: &domain::MerchantKeyStore, - merchant_account: &domain::MerchantAccount, - pm_list_context: ( - PaymentMethodListContext, - Option, - domain::PaymentMethod, - ), - customer: &domain::Customer, - payment_info: Option<&SavedPMLPaymentsInfo>, -) -> Result> { - let (pm_list_context, parent_payment_method_token, pm) = pm_list_context; - let payment_method = pm.payment_method.get_required_value("payment_method")?; - - let bank_details = if payment_method == enums::PaymentMethod::BankDebit { - get_masked_bank_details(&pm).await.unwrap_or_else(|err| { - logger::error!(error=?err); - None - }) - } else { - None - }; - - let payment_method_billing = pm - .payment_method_billing_address - .clone() - .map(|decrypted_data| decrypted_data.into_inner().expose()) - .map(|decrypted_value| decrypted_value.parse_value("payment_method_billing_address")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to parse payment method billing address details")?; - - let connector_mandate_details = pm - .connector_mandate_details - .clone() - .map(|val| val.parse_value::("PaymentsMandateReference")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; - - let (is_connector_agnostic_mit_enabled, requires_cvv, off_session_payment_flag, profile_id) = - payment_info - .map(|pi| { - ( - pi.is_connector_agnostic_mit_enabled, - pi.requires_cvv, - pi.off_session_payment_flag, - pi.business_profile - .as_ref() - .map(|profile| profile.get_id().to_owned()), - ) - }) - .unwrap_or((false, false, false, Default::default())); - - let mca_enabled = get_mca_status( - state, - key_store, - profile_id, - merchant_account.get_id(), - is_connector_agnostic_mit_enabled, - connector_mandate_details, - pm.network_transaction_id.as_ref(), - ) - .await?; - - let requires_cvv = if is_connector_agnostic_mit_enabled { - requires_cvv - && !(off_session_payment_flag - && (pm.connector_mandate_details.is_some() || pm.network_transaction_id.is_some())) - } else { - requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()) - }; - - let pmd = if let Some(card) = pm_list_context.card_details.as_ref() { - Some(api::PaymentMethodListData::Card(card.clone())) - } else if cfg!(feature = "payouts") { - pm_list_context - .bank_transfer_details - .clone() - .map(api::PaymentMethodListData::Bank) - } else { - None - }; - - let pma = api::CustomerPaymentMethod { - payment_token: parent_payment_method_token.clone(), - payment_method_id: pm.get_id().clone(), - customer_id: pm.customer_id.to_owned(), - payment_method, - payment_method_type: pm.payment_method_type, - payment_method_data: pmd, - metadata: pm.metadata.clone(), - recurring_enabled: mca_enabled, - created: Some(pm.created_at), - bank: bank_details, - surcharge_details: None, - requires_cvv: requires_cvv - && !(off_session_payment_flag && pm.connector_mandate_details.is_some()), - last_used_at: Some(pm.last_used_at), - is_default: customer.default_payment_method_id.is_some() - && customer.default_payment_method_id.as_ref() == Some(pm.get_id()), - billing: payment_method_billing, - }; - - payment_info - .async_map(|pi| { - pi.perform_payment_ops(state, parent_payment_method_token, &pma, pm_list_context) - }) - .await - .transpose()?; - - Ok(pma) -} - pub async fn get_mca_status( state: &routes::SessionState, key_store: &domain::MerchantKeyStore, @@ -5236,24 +4873,7 @@ pub async fn get_card_details_with_locker_fallback( pm: &domain::PaymentMethod, state: &routes::SessionState, ) -> errors::RouterResult> { - let card_decrypted = pm - .payment_method_data - .clone() - .map(|x| x.into_inner().expose()) - .and_then(|v| serde_json::from_value::(v).ok()) - .and_then(|pmd| match pmd { - PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), - _ => None, - }); - - Ok(if let Some(crd) = card_decrypted { - Some(crd) - } else { - logger::debug!( - "Getting card details from locker as it is not found in payment methods table" - ); - Some(get_card_details_from_locker(state, pm).await?) - }) + todo!() } #[cfg(all( @@ -5290,25 +4910,13 @@ pub async fn get_card_details_without_locker_fallback( pm: &domain::PaymentMethod, state: &routes::SessionState, ) -> errors::RouterResult { - let card_decrypted = pm - .payment_method_data - .clone() - .map(|x| x.into_inner().expose()) - .and_then(|v| serde_json::from_value::(v).ok()) - .and_then(|pmd| match pmd { - PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), - _ => None, - }); - Ok(if let Some(crd) = card_decrypted { - crd - } else { - logger::debug!( - "Getting card details from locker as it is not found in payment methods table" - ); - get_card_details_from_locker(state, pm).await? - }) + todo!() } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub async fn get_card_details_from_locker( state: &routes::SessionState, pm: &domain::PaymentMethod, @@ -5328,6 +4936,10 @@ pub async fn get_card_details_from_locker( .attach_printable("Get Card Details Failed") } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub async fn get_lookup_key_from_locker( state: &routes::SessionState, payment_token: &str, @@ -5348,7 +4960,7 @@ pub async fn get_lookup_key_from_locker( Ok(resp) } -async fn get_masked_bank_details( +pub async fn get_masked_bank_details( pm: &domain::PaymentMethod, ) -> errors::RouterResult> { let payment_method_data = pm @@ -5377,7 +4989,7 @@ async fn get_masked_bank_details( } } -async fn get_bank_account_connector_details( +pub async fn get_bank_account_connector_details( pm: &domain::PaymentMethod, ) -> errors::RouterResult> { let payment_method_data = pm @@ -5604,8 +5216,16 @@ pub async fn get_bank_from_hs_locker( } } +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub struct TempLockerCardSupport; +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] impl TempLockerCardSupport { #[instrument(skip_all)] async fn create_payment_method_data_in_temp_locker( @@ -5761,53 +5381,7 @@ pub async fn retrieve_payment_method( key_store: domain::MerchantKeyStore, merchant_account: domain::MerchantAccount, ) -> errors::RouterResponse { - let db = state.store.as_ref(); - let pm = db - .find_payment_method( - &((&state).into()), - &key_store, - &pm.payment_method_id, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; - - let card = if pm.payment_method == Some(enums::PaymentMethod::Card) { - let card_detail = if state.conf.locker.locker_enabled { - let card = get_card_from_locker( - &state, - &pm.customer_id, - &pm.merchant_id, - pm.locker_id.as_ref().unwrap_or(pm.get_id()), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting card from card vault")?; - payment_methods::get_card_detail(&pm, card) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details from locker")? - } else { - get_card_details_without_locker_fallback(&pm, &state).await? - }; - Some(card_detail) - } else { - None - }; - Ok(services::ApplicationResponse::Json( - api::PaymentMethodResponse { - merchant_id: pm.merchant_id.to_owned(), - customer_id: pm.customer_id.to_owned(), - payment_method_id: pm.get_id().clone(), - payment_method: pm.payment_method, - payment_method_type: pm.payment_method_type, - metadata: pm.metadata, - created: Some(pm.created_at), - recurring_enabled: false, - payment_method_data: card.clone().map(api::PaymentMethodResponseData::Card), - last_used_at: Some(pm.last_used_at), - client_secret: pm.client_secret, - }, - )) + todo!() } #[cfg(all( diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 1747714149..d0f63a2dc0 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -2,6 +2,8 @@ use std::str::FromStr; use api_models::{enums as api_enums, payment_methods::Card}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use common_utils::ext_traits::ValueExt; use common_utils::{ ext_traits::{Encode, StringExt}, id_type, @@ -249,6 +251,92 @@ pub async fn get_decrypted_response_payload( .attach_printable("Jws Decryption failed for JwsBody for vault") } +pub async fn get_decrypted_vault_response_payload( + jwekey: &settings::Jwekey, + jwe_body: encryption::JweBody, + decryption_scheme: settings::DecryptionScheme, +) -> CustomResult { + let public_key = jwekey.vault_encryption_key.peek().as_bytes(); + + let private_key = jwekey.vault_private_key.peek().as_bytes(); + + let jwt = get_dotted_jwe(jwe_body); + let alg = match decryption_scheme { + settings::DecryptionScheme::RsaOaep => jwe::RSA_OAEP, + settings::DecryptionScheme::RsaOaep256 => jwe::RSA_OAEP_256, + }; + + let jwe_decrypted = encryption::decrypt_jwe( + &jwt, + encryption::KeyIdCheck::SkipKeyIdCheck, + private_key, + alg, + ) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Jwe Decryption failed for JweBody for vault")?; + + let jws = jwe_decrypted + .parse_struct("JwsBody") + .change_context(errors::VaultError::ResponseDeserializationFailed)?; + let jws_body = get_dotted_jws(jws); + + encryption::verify_sign(jws_body, public_key) + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Jws Decryption failed for JwsBody for vault") +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn create_jwe_body_for_vault( + jwekey: &settings::Jwekey, + jws: &str, +) -> CustomResult { + let jws_payload: Vec<&str> = jws.split('.').collect(); + + let generate_jws_body = |payload: Vec<&str>| -> Option { + Some(encryption::JwsBody { + header: payload.first()?.to_string(), + payload: payload.get(1)?.to_string(), + signature: payload.get(2)?.to_string(), + }) + }; + + let jws_body = + generate_jws_body(jws_payload).ok_or(errors::VaultError::RequestEncryptionFailed)?; + + let payload = jws_body + .encode_to_vec() + .change_context(errors::VaultError::RequestEncodingFailed)?; + + let public_key = jwekey.vault_encryption_key.peek().as_bytes(); + + let jwe_encrypted = encryption::encrypt_jwe( + &payload, + public_key, + encryption::EncryptionAlgorithm::A256GCM, + None, + ) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error on jwe encrypt")?; + let jwe_payload: Vec<&str> = jwe_encrypted.split('.').collect(); + + let generate_jwe_body = |payload: Vec<&str>| -> Option { + Some(encryption::JweBody { + header: payload.first()?.to_string(), + iv: payload.get(2)?.to_string(), + encrypted_payload: payload.get(3)?.to_string(), + tag: payload.get(4)?.to_string(), + encrypted_key: payload.get(1)?.to_string(), + }) + }; + + let jwe_body = + generate_jwe_body(jwe_payload).ok_or(errors::VaultError::RequestEncodingFailed)?; + + Ok(jwe_body) +} + pub async fn mk_basilisk_req( jwekey: &settings::Jwekey, jws: &str, @@ -428,14 +516,50 @@ pub fn mk_add_card_response_hs( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub fn mk_add_card_response_hs( - _card: api::CardDetail, - _card_reference: String, - _req: api::PaymentMethodCreate, - _merchant_id: &id_type::MerchantId, + card: api::CardDetail, + card_reference: String, + req: api::PaymentMethodCreate, + merchant_id: &id_type::MerchantId, ) -> api::PaymentMethodResponse { todo!() } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub fn generate_payment_method_response( + pm: &domain::PaymentMethod, +) -> errors::RouterResult { + let pmd = pm + .payment_method_data + .clone() + .map(|data| data.into_inner().expose()) + .map(|decrypted_value| decrypted_value.parse_value("PaymentMethodsData")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to parse PaymentMethodsData")? + .and_then(|data| match data { + api::PaymentMethodsData::Card(card) => { + Some(api::PaymentMethodResponseData::Card(card.into())) + } + _ => None, + }); + + let resp = api::PaymentMethodResponse { + merchant_id: pm.merchant_id.to_owned(), + customer_id: pm.customer_id.to_owned(), + payment_method_id: pm.id.get_string_repr(), + payment_method: pm.payment_method, + payment_method_type: pm.payment_method_type, + metadata: pm.metadata.clone(), + created: Some(pm.created_at), + recurring_enabled: false, + last_used_at: Some(pm.last_used_at), + client_secret: pm.client_secret.clone(), + payment_method_data: pmd, + }; + + Ok(resp) +} + #[allow(clippy::too_many_arguments)] pub async fn mk_get_card_request_hs( jwekey: &settings::Jwekey, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 0ec14bd69e..0af5bf6037 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3807,6 +3807,25 @@ where .await } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn decide_multiplex_connector_for_normal_or_recurring_payment( + state: &SessionState, + payment_data: &mut D, + routing_data: &mut storage::RoutingData, + connectors: Vec, + mandate_type: Option, + is_connector_agnostic_mit_enabled: Option, +) -> RouterResult +where + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ + todo!() +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] #[allow(clippy::too_many_arguments)] pub async fn decide_multiplex_connector_for_normal_or_recurring_payment( state: &SessionState, @@ -3970,6 +3989,26 @@ where } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[allow(clippy::too_many_arguments)] +pub async fn decide_connector_for_normal_or_recurring_payment( + state: &SessionState, + payment_data: &mut D, + routing_data: &mut storage::RoutingData, + connectors: Vec, + is_connector_agnostic_mit_enabled: Option, + payment_method_info: &domain::PaymentMethod, +) -> RouterResult +where + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ + todo!() +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] #[allow(clippy::too_many_arguments)] pub async fn decide_connector_for_normal_or_recurring_payment( state: &SessionState, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index a1d92d8a99..ce92c1ab00 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1781,6 +1781,23 @@ pub async fn retrieve_payment_method_with_temporary_token( }) } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn retrieve_card_with_permanent_token( + state: &SessionState, + locker_id: &str, + _payment_method_id: &common_utils::id_type::GlobalPaymentMethodId, + payment_intent: &PaymentIntent, + card_token_data: Option<&domain::CardToken>, + _merchant_key_store: &domain::MerchantKeyStore, + _storage_scheme: enums::MerchantStorageScheme, +) -> RouterResult { + todo!() +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] #[allow(clippy::too_many_arguments)] pub async fn retrieve_card_with_permanent_token( state: &SessionState, @@ -1986,6 +2003,20 @@ pub async fn fetch_card_details_from_locker( Ok(domain::PaymentMethodData::Card(api_card.into())) } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn retrieve_payment_method_from_db_with_token_data( + state: &SessionState, + merchant_key_store: &domain::MerchantKeyStore, + token_data: &storage::PaymentTokenData, + storage_scheme: storage::enums::MerchantStorageScheme, +) -> RouterResult> { + todo!() +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] pub async fn retrieve_payment_method_from_db_with_token_data( state: &SessionState, merchant_key_store: &domain::MerchantKeyStore, @@ -2082,6 +2113,27 @@ pub async fn retrieve_payment_token_data( Ok(token_data) } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn make_pm_data<'a, F: Clone, R, D>( + _operation: BoxedOperation<'a, F, R, D>, + _state: &'a SessionState, + _payment_data: &mut PaymentData, + _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, + _storage_scheme: common_enums::enums::MerchantStorageScheme, + _business_profile: Option<&domain::BusinessProfile>, +) -> RouterResult<( + BoxedOperation<'a, F, R, D>, + Option, + Option, +)> { + todo!() +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn make_pm_data<'a, F: Clone, R, D>( operation: BoxedOperation<'a, F, R, D>, state: &'a SessionState, @@ -5100,6 +5152,21 @@ pub fn update_additional_payment_data_with_connector_response_pm_data( .attach_printable("Failed to encode additional pm data") } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub async fn get_payment_method_details_from_payment_token( + state: &SessionState, + payment_attempt: &PaymentAttempt, + payment_intent: &PaymentIntent, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, +) -> RouterResult> { + todo!() +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn get_payment_method_details_from_payment_token( state: &SessionState, payment_attempt: &PaymentAttempt, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index f397ecd8a2..a9cd1ae4d8 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -978,6 +978,35 @@ impl ValidateRequest> f } impl PaymentCreate { + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + #[instrument(skip_all)] + #[allow(clippy::too_many_arguments)] + pub async fn make_payment_attempt( + payment_id: &common_utils::id_type::PaymentId, + merchant_id: &common_utils::id_type::MerchantId, + organization_id: &common_utils::id_type::OrganizationId, + money: (api::Amount, enums::Currency), + payment_method: Option, + payment_method_type: Option, + request: &api::PaymentsRequest, + browser_info: Option, + state: &SessionState, + payment_method_billing_address_id: Option, + payment_method_info: &Option, + _key_store: &domain::MerchantKeyStore, + profile_id: common_utils::id_type::ProfileId, + customer_acceptance: &Option, + ) -> RouterResult<( + storage::PaymentAttemptNew, + Option, + )> { + todo!() + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn make_payment_attempt( diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index fb09b4d8b6..ae6ac33067 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -88,6 +88,26 @@ impl PostUpdateTracker, types::PaymentsAuthor Ok(payment_data) } + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + async fn save_pm_and_mandate<'b>( + &self, + state: &SessionState, + resp: &types::RouterData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, + business_profile: &domain::BusinessProfile, + ) -> CustomResult<(), errors::ApiErrorResponse> + where + F: 'b + Clone + Send + Sync, + { + todo!() + } + + #[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") + ))] async fn save_pm_and_mandate<'b>( &self, state: &SessionState, @@ -1602,6 +1622,23 @@ async fn payment_response_update_tracker( } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +async fn update_payment_method_status_and_ntid( + state: &SessionState, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentData, + attempt_status: common_enums::AttemptStatus, + payment_response: Result, + storage_scheme: enums::MerchantStorageScheme, + is_connector_agnostic_mit_enabled: Option, +) -> RouterResult<()> { + todo!() +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] async fn update_payment_method_status_and_ntid( state: &SessionState, key_store: &domain::MerchantKeyStore, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d81258d667..0760cd449f 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -214,6 +214,28 @@ impl GetTracker, api::PaymentsRetrieveRequest } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +async fn get_tracker_for_sync< + 'a, + F: Send + Clone, + Op: Operation> + 'a + Send + Sync, +>( + _payment_id: &api::PaymentIdType, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _state: &SessionState, + _request: &api::PaymentsRetrieveRequest, + _operation: Op, + _storage_scheme: enums::MerchantStorageScheme, +) -> RouterResult>> +{ + todo!() +} + +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") +))] async fn get_tracker_for_sync< 'a, F: Send + Clone, diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index f948d7faaf..2a93f8c215 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -489,7 +489,18 @@ async fn store_bank_details_in_payment_methods( .await .change_context(ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt customer details")?; + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] let pm_id = generate_id(consts::ID_LENGTH, "pm"); + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + let pm_id = common_utils::id_type::GlobalPaymentMethodId::generate("random_cell_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to generate GlobalPaymentMethodId")?; + let now = common_utils::date_time::now(); #[cfg(all( any(feature = "v1", feature = "v2"), diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 4dc7721185..8120ccc808 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1821,6 +1821,10 @@ impl PaymentIntentInterface for KafkaStore { #[async_trait::async_trait] impl PaymentMethodInterface for KafkaStore { + #[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") + ))] async fn find_payment_method( &self, state: &KeyManagerState, @@ -1833,6 +1837,19 @@ impl PaymentMethodInterface for KafkaStore { .await } + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + async fn find_payment_method( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + payment_method_id: &id_type::GlobalPaymentMethodId, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_method(state, key_store, payment_method_id, storage_scheme) + .await + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index cf263def41..6e05207679 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -14,6 +14,10 @@ use crate::{ #[async_trait::async_trait] pub trait PaymentMethodInterface { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] async fn find_payment_method( &self, state: &KeyManagerState, @@ -22,6 +26,15 @@ pub trait PaymentMethodInterface { storage_scheme: MerchantStorageScheme, ) -> CustomResult; + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_payment_method( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + payment_method_id: &id_type::GlobalPaymentMethodId, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult; + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -231,7 +244,7 @@ mod storage { &self, state: &KeyManagerState, key_store: &domain::MerchantKeyStore, - payment_method_id: &str, + payment_method_id: &id_type::GlobalPaymentMethodId, storage_scheme: MerchantStorageScheme, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; @@ -251,7 +264,8 @@ mod storage { match storage_scheme { MerchantStorageScheme::PostgresOnly => database_call().await, MerchantStorageScheme::RedisKv => { - let lookup_id = format!("payment_method_{}", payment_method_id); + let lookup_id = + format!("payment_method_{}", payment_method_id.get_string_repr()); let lookup = fallback_reverse_lookup_not_found!( self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await, @@ -385,6 +399,38 @@ mod storage { .map_err(|error| report!(errors::StorageError::from(error))) } + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + #[instrument(skip_all)] + async fn insert_payment_method( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + payment_method: domain::PaymentMethod, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let payment_method_new = payment_method + .construct_new() + .await + .change_context(errors::StorageError::DecryptionError)?; + + let conn = connection::pg_connection_write(self).await?; + payment_method_new + .insert(&conn) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] #[instrument(skip_all)] async fn insert_payment_method( &self, @@ -591,77 +637,18 @@ mod storage { .await .change_context(errors::StorageError::DecryptionError)?; - let merchant_id = payment_method.merchant_id.clone(); - let customer_id = payment_method.customer_id.clone(); - let key = PartitionKey::MerchantIdCustomerId { - merchant_id: &merchant_id, - customer_id: &customer_id, - }; - let field = format!("payment_method_id_{}", payment_method.get_id()); - let storage_scheme = - Box::pin(decide_storage_scheme::<_, storage_types::PaymentMethod>( - self, - storage_scheme, - Op::Update(key.clone(), &field, payment_method.updated_by.as_deref()), - )) - .await; - let pm = match storage_scheme { - MerchantStorageScheme::PostgresOnly => { - let conn = connection::pg_connection_write(self).await?; - payment_method - .update_with_id( - &conn, - payment_method_update.convert_to_payment_method_update(storage_scheme), - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - MerchantStorageScheme::RedisKv => { - let key_str = key.to_string(); - - let p_update: PaymentMethodUpdateInternal = - payment_method_update.convert_to_payment_method_update(storage_scheme); - let updated_payment_method = - p_update.clone().apply_changeset(payment_method.clone()); - - let redis_value = serde_json::to_string(&updated_payment_method) - .change_context(errors::StorageError::SerializationFailed)?; - - let redis_entry = kv::TypedSql { - op: kv::DBOperation::Update { - updatable: kv::Updateable::PaymentMethodUpdate( - kv::PaymentMethodUpdateMems { - orig: payment_method, - update_data: p_update, - }, - ), - }, - }; - - kv_wrapper::<(), _, _>( - self, - KvOperation::::Hset( - (&field, redis_value), - redis_entry, - ), - key, - ) - .await - .map_err(|err| err.to_redis_failed_response(&key_str))? - .try_into_hset() - .change_context(errors::StorageError::KVError)?; - - Ok(updated_payment_method) - } - }?; - - pm.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) + let conn = connection::pg_connection_write(self).await?; + payment_method + .update_with_id(&conn, payment_method_update.into()) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) } #[cfg(all( @@ -927,7 +914,7 @@ mod storage { &self, state: &KeyManagerState, key_store: &domain::MerchantKeyStore, - payment_method_id: &str, + payment_method_id: &id_type::GlobalPaymentMethodId, _storage_scheme: MerchantStorageScheme, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; @@ -1258,6 +1245,10 @@ mod storage { #[async_trait::async_trait] impl PaymentMethodInterface for MockDb { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] async fn find_payment_method( &self, state: &KeyManagerState, @@ -1287,6 +1278,36 @@ impl PaymentMethodInterface for MockDb { } } + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + async fn find_payment_method( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + payment_method_id: &id_type::GlobalPaymentMethodId, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let payment_methods = self.payment_methods.lock().await; + let payment_method = payment_methods + .iter() + .find(|pm| pm.get_id() == payment_method_id) + .cloned(); + + match payment_method { + Some(pm) => Ok(pm + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError)?), + None => Err(errors::StorageError::ValueNotFound( + "cannot find payment method".to_string(), + ) + .into()), + } + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 2bafe504dd..aad5acbbad 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1064,6 +1064,24 @@ impl Payouts { } } +#[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2",))] +impl PaymentMethods { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/v2/payment_methods").app_data(web::Data::new(state)); + route = route + .service(web::resource("").route(web::post().to(create_payment_method_api))) + .service( + web::resource("/create-intent") + .route(web::post().to(create_payment_method_intent_api)), + ) + .service( + web::resource("/{id}/confirm-intent") + .route(web::post().to(confirm_payment_method_intent_api)), + ); + + route + } +} pub struct PaymentMethods; #[cfg(all( diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index e2cb50b0aa..725d6af81a 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -12,6 +12,11 @@ use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore; use router_env::{instrument, logger, tracing, Flow}; use super::app::{AppState, SessionState}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::core::payment_methods::{ + create_payment_method, list_customer_payment_method_util, payment_method_intent_confirm, + payment_method_intent_create, +}; use crate::{ core::{ api_locking, errors, @@ -35,6 +40,10 @@ use crate::{ types::api::customers::CustomerRequest, }; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] pub async fn create_payment_method_api( state: web::Data, @@ -63,6 +72,114 @@ pub async fn create_payment_method_api( .await } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] +pub async fn create_payment_method_api( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::PaymentMethodsCreate; + + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth, req, _| async move { + Box::pin(create_payment_method( + &state, + req, + &auth.merchant_account, + &auth.key_store, + )) + .await + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] +pub async fn create_payment_method_intent_api( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::PaymentMethodsCreate; + + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth, req, _| async move { + Box::pin(payment_method_intent_create( + &state, + req, + &auth.merchant_account, + &auth.key_store, + )) + .await + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] +pub async fn confirm_payment_method_intent_api( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> HttpResponse { + let flow = Flow::PaymentMethodsCreate; + let pm_id = path.into_inner(); + let payload = json_payload.into_inner(); + + let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return api::log_and_return_error_response(e), + }; + + let inner_payload = payment_methods::PaymentMethodIntentConfirmInternal { + id: pm_id.clone(), + payment_method: payload.payment_method, + payment_method_type: payload.payment_method_type, + client_secret: payload.client_secret.clone(), + customer_id: payload.customer_id.to_owned(), + payment_method_data: payload.payment_method_data.clone(), + }; + + Box::pin(api::server_wrap( + flow, + state, + &req, + inner_payload, + |state, auth, req, _| { + let pm_id = pm_id.clone(); + async move { + Box::pin(payment_method_intent_confirm( + &state, + req.into(), + &auth.merchant_account, + &auth.key_store, + pm_id, + )) + .await + } + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsMigrate))] pub async fn migrate_payment_method_api( state: web::Data, @@ -346,7 +463,7 @@ pub async fn list_customer_payment_method_for_payment( &req, payload, |state, auth, req, _| { - cards::list_customer_payment_method_util( + list_customer_payment_method_util( state, auth.merchant_account, auth.key_store, @@ -411,7 +528,7 @@ pub async fn list_customer_payment_method_api( &req, payload, |state, auth, req, _| { - cards::list_customer_payment_method_util( + list_customer_payment_method_util( state, auth.merchant_account, auth.key_store, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 16a8ed1ccc..bcc2046e24 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -11,6 +11,7 @@ pub mod authentication; pub mod domain; #[cfg(feature = "frm")] pub mod fraud_check; +pub mod payment_methods; pub mod pm_auth; use masking::Secret; pub mod storage; diff --git a/crates/router/src/types/api/mandates.rs b/crates/router/src/types/api/mandates.rs index 9262877a38..b3bcfa2a88 100644 --- a/crates/router/src/types/api/mandates.rs +++ b/crates/router/src/types/api/mandates.rs @@ -125,75 +125,7 @@ impl MandateResponseExt for MandateResponse { mandate: storage::Mandate, storage_scheme: storage_enums::MerchantStorageScheme, ) -> RouterResult { - let db = &*state.store; - let payment_method = db - .find_payment_method( - &(state.into()), - &key_store, - &mandate.payment_method_id, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; - - let pm = payment_method - .payment_method - .get_required_value("payment_method") - .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) - .attach_printable("payment_method not found")?; - - let card = if pm == storage_enums::PaymentMethod::Card { - // if locker is disabled , decrypt the payment method data - let card_details = if state.conf.locker.locker_enabled { - let card = payment_methods::cards::get_card_from_locker( - state, - &payment_method.customer_id, - &payment_method.merchant_id, - payment_method - .locker_id - .as_ref() - .unwrap_or(&payment_method.id), - ) - .await?; - - payment_methods::transformers::get_card_detail(&payment_method, card) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details")? - } else { - payment_methods::cards::get_card_details_without_locker_fallback( - &payment_method, - state, - ) - .await? - }; - - Some(MandateCardDetails::from(card_details).into_inner()) - } else { - None - }; - let payment_method_type = payment_method - .payment_method_type - .map(|pmt| pmt.to_string()); - Ok(Self { - mandate_id: mandate.mandate_id, - customer_acceptance: Some(api::payments::CustomerAcceptance { - acceptance_type: if mandate.customer_ip_address.is_some() { - api::payments::AcceptanceType::Online - } else { - api::payments::AcceptanceType::Offline - }, - accepted_at: mandate.customer_accepted_at, - online: Some(api::payments::OnlineMandate { - ip_address: mandate.customer_ip_address, - user_agent: mandate.customer_user_agent.unwrap_or_default(), - }), - }), - card, - status: mandate.mandate_status, - payment_method: pm.to_string(), - payment_method_type, - payment_method_id: mandate.payment_method_id, - }) + todo!() } } diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index f4d1492fb4..65111815b5 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -5,8 +5,8 @@ pub use api_models::payment_methods::{ GetTokenizePayloadRequest, GetTokenizePayloadResponse, ListCountriesCurrenciesRequest, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, - PaymentMethodIntentConfirm, PaymentMethodIntentCreate, PaymentMethodList, - PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse, + PaymentMethodIntentConfirm, PaymentMethodIntentConfirmInternal, PaymentMethodIntentCreate, + PaymentMethodList, PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate, PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, @@ -32,6 +32,8 @@ use crate::core::{ errors::{self, RouterResult}, payments::helpers::validate_payment_method_type_against_payment_method, }; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::utils; pub(crate) trait PaymentMethodCreateExt { fn validate(&self) -> RouterResult<()>; @@ -61,15 +63,63 @@ impl PaymentMethodCreateExt for PaymentMethodCreate { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl PaymentMethodCreateExt for PaymentMethodCreate { fn validate(&self) -> RouterResult<()> { - if !validate_payment_method_type_against_payment_method( - self.payment_method, - self.payment_method_type, - ) { - return Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid 'payment_method_type' provided".to_string() - }) - .attach_printable("Invalid payment method type")); - } + utils::when( + !validate_payment_method_type_against_payment_method( + self.payment_method, + self.payment_method_type, + ), + || { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid 'payment_method_type' provided".to_string() + }) + .attach_printable("Invalid payment method type")); + }, + ); + + utils::when( + !PaymentMethodCreate::validate_payment_method_data_against_payment_method( + self.payment_method, + self.payment_method_data.clone(), + ), + || { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid 'payment_method_data' provided".to_string() + }) + .attach_printable("Invalid payment method data")); + }, + ); + Ok(()) + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl PaymentMethodCreateExt for PaymentMethodIntentConfirm { + fn validate(&self) -> RouterResult<()> { + utils::when( + !validate_payment_method_type_against_payment_method( + self.payment_method, + self.payment_method_type, + ), + || { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid 'payment_method_type' provided".to_string() + }) + .attach_printable("Invalid payment method type")); + }, + ); + + utils::when( + !PaymentMethodIntentConfirm::validate_payment_method_data_against_payment_method( + self.payment_method, + self.payment_method_data.clone(), + ), + || { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid 'payment_method_data' provided".to_string() + }) + .attach_printable("Invalid payment method data")); + }, + ); Ok(()) } } diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs new file mode 100644 index 0000000000..933b8e73da --- /dev/null +++ b/crates/router/src/types/payment_methods.rs @@ -0,0 +1,103 @@ +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use common_utils::generate_id; + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use crate::{ + consts, + types::{api, domain, storage}, +}; + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[async_trait::async_trait] +pub trait VaultingInterface { + fn get_vaulting_request_url() -> &'static str; +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[async_trait::async_trait] +pub trait VaultingDataInterface { + fn get_vaulting_data_key(&self) -> String; +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct VaultFingerprintRequest { + pub data: String, + pub key: String, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct VaultFingerprintResponse { + pub fingerprint_id: String, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct AddVaultRequest { + pub entity_id: common_utils::id_type::MerchantId, + pub vault_id: String, + pub data: D, + pub ttl: i64, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct AddVaultResponse { + pub entity_id: common_utils::id_type::MerchantId, + pub vault_id: String, + pub fingerprint_id: Option, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct AddVault; + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetVaultFingerprint; + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[async_trait::async_trait] +impl VaultingInterface for AddVault { + fn get_vaulting_request_url() -> &'static str { + consts::ADD_VAULT_REQUEST_URL + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[async_trait::async_trait] +impl VaultingInterface for GetVaultFingerprint { + fn get_vaulting_request_url() -> &'static str { + consts::VAULT_FINGERPRINT_REQUEST_URL + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[async_trait::async_trait] +impl VaultingDataInterface for api::PaymentMethodCreateData { + fn get_vaulting_data_key(&self) -> String { + match &self { + api::PaymentMethodCreateData::Card(card) => card.card_number.to_string(), + } + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub struct PaymentMethodClientSecret; + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl PaymentMethodClientSecret { + pub fn generate(payment_method_id: &common_utils::id_type::GlobalPaymentMethodId) -> String { + todo!() + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +pub struct SavedPMLPaymentsInfo { + pub payment_intent: storage::PaymentIntent, + pub business_profile: Option, + pub requires_cvv: bool, + pub off_session_payment_flag: bool, + pub is_connector_agnostic_mit_enabled: bool, +} diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 5bbfd8483c..34c5714e12 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -19,6 +19,10 @@ pub enum PaymentTokenKind { Permanent, } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CardTokenData { pub payment_method_id: Option, @@ -27,6 +31,14 @@ pub struct CardTokenData { pub network_token_locker_id: Option, } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CardTokenData { + pub payment_method_id: Option, + pub locker_id: Option, + pub token: String, +} + #[derive(Debug, Clone, serde::Serialize, Default, serde::Deserialize)] pub struct PaymentMethodDataWithId { pub payment_method: Option, @@ -58,6 +70,10 @@ pub enum PaymentTokenData { } impl PaymentTokenData { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] pub fn permanent_card( payment_method_id: Option, locker_id: Option, @@ -72,6 +88,19 @@ impl PaymentTokenData { }) } + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + pub fn permanent_card( + payment_method_id: Option, + locker_id: Option, + token: String, + ) -> Self { + Self::PermanentCard(CardTokenData { + payment_method_id, + locker_id, + token, + }) + } + pub fn temporary_generic(token: String) -> Self { Self::TemporaryGeneric(GenericTokenData { token }) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 1442f5b8d4..a6f6d7c4c5 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -129,19 +129,7 @@ impl domain::PaymentMethod, ), ) -> Self { - Self { - merchant_id: item.merchant_id.to_owned(), - customer_id: item.customer_id.to_owned(), - payment_method_id: item.get_id().clone(), - payment_method: item.payment_method, - payment_method_type: item.payment_method_type, - payment_method_data: card_details.map(payment_methods::PaymentMethodResponseData::Card), - recurring_enabled: false, - metadata: item.metadata, - created: Some(item.created_at), - last_used_at: None, - client_secret: item.client_secret, - } + todo!() } } diff --git a/crates/router/src/workflows/payment_method_status_update.rs b/crates/router/src/workflows/payment_method_status_update.rs index abc30074cd..124417e335 100644 --- a/crates/router/src/workflows/payment_method_status_update.rs +++ b/crates/router/src/workflows/payment_method_status_update.rs @@ -14,6 +14,10 @@ pub struct PaymentMethodStatusUpdateWorkflow; #[async_trait::async_trait] impl ProcessTrackerWorkflow for PaymentMethodStatusUpdateWorkflow { + #[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2") + ))] async fn execute_workflow<'a>( &'a self, state: &'a SessionState, diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 6a79638677..cadb78944e 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -437,7 +437,7 @@ impl UniqueConstraints for diesel_models::PaymentMethod { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl UniqueConstraints for diesel_models::PaymentMethod { fn unique_constraints(&self) -> Vec { - vec![format!("paymentmethod_{}", self.id)] + vec![self.id.get_string_repr()] } fn table_name(&self) -> &str { "PaymentMethod"