diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 636723d808a4..bc0f37a31deb 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -17092,6 +17092,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } }, "additionalProperties": false @@ -17331,6 +17336,11 @@ "example": "Invalid card details", "nullable": true, "maxLength": 1024 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } }, "additionalProperties": false diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 186aa6dc9c3b..03edecc70f2a 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -20886,6 +20886,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } } }, @@ -21184,6 +21189,11 @@ "example": "Invalid card details", "nullable": true, "maxLength": 1024 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } }, "additionalProperties": false @@ -21830,6 +21840,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } } }, @@ -22042,6 +22057,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } } }, diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 18d18f08fd24..814d688e15a6 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -276,7 +276,8 @@ pub struct PaymentMethodMigrate { pub billing: Option, /// The connector mandate details of the payment method - pub connector_mandate_details: Option, + #[serde(deserialize_with = "deserialize_connector_mandate_details")] + pub connector_mandate_details: Option, // The CIT (customer initiated transaction) transaction id associated with the payment method pub network_transaction_id: Option, @@ -300,12 +301,22 @@ pub struct PaymentMethodMigrateResponse { pub network_transaction_id_migrated: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentsMandateReference( pub HashMap, ); -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct PayoutsMandateReference( + pub HashMap, +); + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct PayoutsMandateReferenceRecord { + pub transfer_method_id: Option, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PaymentsMandateReferenceRecord { pub connector_mandate_id: String, pub payment_method_type: Option, @@ -313,6 +324,69 @@ pub struct PaymentsMandateReferenceRecord { pub original_payment_authorized_currency: Option, } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct CommonMandateReference { + pub payments: Option, + pub payouts: Option, +} + +impl From for PaymentsMandateReference { + fn from(common_mandate: CommonMandateReference) -> Self { + common_mandate.payments.unwrap_or_default() + } +} + +impl From for CommonMandateReference { + fn from(payments_reference: PaymentsMandateReference) -> Self { + Self { + payments: Some(payments_reference), + payouts: None, + } + } +} + +fn deserialize_connector_mandate_details<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value: Option = + as de::Deserialize>::deserialize(deserializer)?; + if let Some(connector_mandate_value) = value { + let mut payments_data = None; + let mut payouts_data = None; + + if let Some(obj) = connector_mandate_value.clone().as_object_mut() { + obj.remove("payouts"); + + if let Ok(payment_mandate_record) = + serde_json::from_value::(serde_json::json!(obj)) + { + payments_data = Some(payment_mandate_record); + } + } + if let Ok(payment_mandate_record) = + serde_json::from_value::(connector_mandate_value) + { + payouts_data = payment_mandate_record.payouts; + } + + if payments_data.is_none() && payouts_data.is_none() { + Err(de::Error::custom( + "Failed to deserialize connector_mandate_details", + )) + } else { + Ok(Some(CommonMandateReference { + payments: payments_data, + payouts: payouts_data, + })) + } + } else { + Ok(None) + } +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -346,7 +420,12 @@ impl PaymentMethodCreate { payment_method_issuer_code: payment_method_migrate.payment_method_issuer_code, metadata: payment_method_migrate.metadata.clone(), payment_method_data: payment_method_migrate.payment_method_data.clone(), - connector_mandate_details: payment_method_migrate.connector_mandate_details.clone(), + connector_mandate_details: payment_method_migrate + .connector_mandate_details + .clone() + .map(|common_mandate_reference| { + PaymentsMandateReference::from(common_mandate_reference) + }), client_secret: None, billing: payment_method_migrate.billing.clone(), card: card_details, @@ -2352,7 +2431,11 @@ impl }), email: record.email, }), - connector_mandate_details, + connector_mandate_details: connector_mandate_details.map( + |payments_mandate_reference| { + CommonMandateReference::from(payments_mandate_reference) + }, + ), metadata: None, payment_method_issuer_code: None, card_network: None, diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 237d165d572c..4c8a73b75a8d 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -184,6 +184,9 @@ pub struct PayoutCreateRequest { /// Customer's phone country code. _Deprecated: Use customer object instead._ #[schema(deprecated, max_length = 255, example = "+1")] pub phone_country_code: Option, + + /// Identifier for payout method + pub payout_method_id: Option, } impl PayoutCreateRequest { @@ -568,6 +571,9 @@ pub struct PayoutCreateResponse { #[remove_in(PayoutCreateResponse)] #[schema(value_type = Option, max_length = 1024, example = "Invalid card details")] pub unified_message: Option, + + /// Identifier for payout method + pub payout_method_id: Option, } /// The payout method information for response diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 76613984363d..d24bd1ead165 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use common_enums::MerchantStorageScheme; -use common_utils::{encryption::Encryption, pii}; +use common_utils::{encryption::Encryption, errors::ParsingError, pii}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use error_stack::report; #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -77,7 +78,7 @@ pub struct PaymentMethod { pub payment_method_data: Option, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -165,7 +166,7 @@ pub struct PaymentMethodNew { pub payment_method_data: Option, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -293,7 +294,7 @@ pub enum PaymentMethodUpdate { locker_fingerprint_id: Option, }, ConnectorMandateDetailsUpdate { - connector_mandate_details: Option, + connector_mandate_details: Option, }, } @@ -318,7 +319,7 @@ pub struct PaymentMethodUpdateInternal { status: Option, locker_id: Option, payment_method_type_v2: Option, - connector_mandate_details: Option, + connector_mandate_details: Option, updated_by: Option, payment_method_subtype: Option, last_modified: PrimitiveDateTime, @@ -970,3 +971,110 @@ impl std::ops::DerefMut for PaymentsMandateReference { } common_utils::impl_to_sql_from_sql_json!(PaymentsMandateReference); + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct PayoutsMandateReferenceRecord { + pub transfer_method_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct PayoutsMandateReference( + pub HashMap, +); + +impl std::ops::Deref for PayoutsMandateReference { + type Target = + HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for PayoutsMandateReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct CommonMandateReference { + pub payments: Option, + pub payouts: Option, +} + +impl diesel::serialize::ToSql for CommonMandateReference { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, diesel::pg::Pg>, + ) -> diesel::serialize::Result { + let mut payments = serde_json::to_value(self.payments.as_ref())?; + let payouts = serde_json::to_value(self.payouts.as_ref())?; + + if let Some(payments_object) = payments.as_object_mut() { + if !payouts.is_null() { + payments_object.insert("payouts".to_string(), payouts); + } + } + + >::to_sql(&payments, &mut out.reborrow()) + } +} + +impl diesel::deserialize::FromSql + for CommonMandateReference +where + serde_json::Value: diesel::deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + + let mut payments_data = None; + let mut payouts_data = None; + + if let Some(obj) = value.clone().as_object_mut() { + obj.remove("payouts"); + + if let Ok(payment_mandate_record) = + serde_json::from_value::(serde_json::json!(obj)) + { + payments_data = Some(payment_mandate_record); + } + } + + if let Ok(payment_mandate_record) = serde_json::from_value::(value.clone()) { + payouts_data = payment_mandate_record.payouts + } + + if payments_data.is_none() && payouts_data.is_none() { + Err( + report!(ParsingError::StructParseFailure("CommonMandateReference")) + .attach_printable( + "Failed to parse JSON into CommonMandateReference or PaymentsMandateReference", + ), + )? + } else { + Ok(Self { + payments: payments_data, + payouts: payouts_data, + }) + } + } +} + +impl From for CommonMandateReference { + fn from(payment_reference: PaymentsMandateReference) -> Self { + Self { + payments: Some(payment_reference), + payouts: None, + } + } +} diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 9061a758beab..62db4d924601 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -15,6 +15,8 @@ use common_utils::{ types::{AmountConvertor, MinorUnit}, }; use error_stack::{report, ResultExt}; +#[cfg(feature = "payouts")] +use hyperswitch_domain_models::router_request_types::PayoutsData; use hyperswitch_domain_models::{ address::{Address, AddressDetails, PhoneDetails}, payment_method_data::{self, Card, PaymentMethodData}, @@ -1239,6 +1241,26 @@ impl PhoneDetailsData for PhoneDetails { } } +#[cfg(feature = "payouts")] +pub trait PayoutFulfillRequestData { + fn get_connector_payout_id(&self) -> Result; + fn get_connector_transfer_method_id(&self) -> Result; +} +#[cfg(feature = "payouts")] +impl PayoutFulfillRequestData for PayoutsData { + fn get_connector_payout_id(&self) -> Result { + self.connector_payout_id + .clone() + .ok_or_else(missing_field_err("connector_payout_id")) + } + + fn get_connector_transfer_method_id(&self) -> Result { + self.connector_transfer_method_id + .clone() + .ok_or_else(missing_field_err("connector_transfer_method_id")) + } +} + pub trait CustomerData { fn get_email(&self) -> Result; } diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index 7789c05e9902..0f9fdd3cbbe2 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -659,7 +659,7 @@ common_utils::create_list_wrapper!( pub fn is_merchant_connector_account_id_in_connector_mandate_details( &self, profile_id: Option<&id_type::ProfileId>, - connector_mandate_details: &diesel_models::PaymentsMandateReference, + connector_mandate_details: &diesel_models::CommonMandateReference, ) -> bool { let mca_ids = self .iter() @@ -671,8 +671,11 @@ common_utils::create_list_wrapper!( .collect::>(); connector_mandate_details - .keys() - .any(|mca_id| mca_ids.contains(mca_id)) + .payments + .as_ref() + .map_or(false, |payments| { + payments.0.keys().any(|mca_id| mca_ids.contains(mca_id)) + }) } } ); diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index d03762dd64e8..f85696889af6 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -2,7 +2,7 @@ use common_utils::crypto::Encryptable; use common_utils::{ crypto::OptionalEncryptableValue, - errors::{CustomResult, ValidationError}, + errors::{CustomResult, ParsingError, ValidationError}, pii, type_name, types::keymanager, }; @@ -84,7 +84,7 @@ pub struct PaymentMethod { OptionalEncryptableJsonType, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -138,6 +138,65 @@ impl PaymentMethod { pub fn get_payment_method_subtype(&self) -> Option { self.payment_method_subtype } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + pub fn get_common_mandate_reference( + &self, + ) -> Result { + if let Some(value) = &self.connector_mandate_details { + let mut payments_data = None; + let mut payouts_data = None; + + if let Some(obj) = value.clone().as_object_mut() { + obj.remove("payouts"); + + if let Ok(payment_mandate_record) = serde_json::from_value::< + diesel_models::PaymentsMandateReference, + >(serde_json::json!(obj)) + { + payments_data = Some(payment_mandate_record); + } + } + if let Ok(payment_mandate_record) = + serde_json::from_value::(value.clone()) + { + payouts_data = payment_mandate_record.payouts + } + + if payments_data.is_none() && payouts_data.is_none() { + Err(ParsingError::StructParseFailure( + "Failed to deserialize PaymentMethod", + ))? + } else { + Ok(diesel_models::CommonMandateReference { + payments: payments_data, + payouts: payouts_data, + }) + } + } else { + Ok(diesel_models::CommonMandateReference { + payments: None, + payouts: None, + }) + } + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + pub fn get_common_mandate_reference( + &self, + ) -> Result { + if let Some(value) = &self.connector_mandate_details { + Ok(value.clone()) + } else { + Ok(diesel_models::CommonMandateReference { + payments: None, + payouts: None, + }) + } + } } #[cfg(all( diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index aa9fb2b5f1f0..c5771d7af66f 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -800,6 +800,7 @@ pub struct PayoutsData { // New minor amount for amount framework pub minor_amount: MinorUnit, pub priority: Option, + pub connector_transfer_method_id: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 6cdadf07320c..a7f0a4b113fb 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -1150,7 +1150,7 @@ pub async fn create_payment_method_in_db( api::payment_methods::PaymentMethodsData, >, key_store: &domain::MerchantKeyStore, - connector_mandate_details: Option, + connector_mandate_details: Option, status: Option, network_transaction_id: Option, storage_scheme: enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 164c0e9557af..169b9191f499 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -846,9 +846,10 @@ pub async fn skip_locker_call_and_migrate_payment_method( .clone() .and_then(|val| if val == json!({}) { None } else { Some(true) }) .or_else(|| { - req.connector_mandate_details - .clone() - .and_then(|val| (!val.0.is_empty()).then_some(false)) + req.connector_mandate_details.clone().and_then(|val| { + val.payments + .and_then(|payin_val| (!payin_val.0.is_empty()).then_some(false)) + }) }), ); @@ -2819,7 +2820,7 @@ pub async fn update_payment_method_connector_mandate_details( key_store: &domain::MerchantKeyStore, db: &dyn db::StorageInterface, pm: domain::PaymentMethod, - connector_mandate_details: Option, + connector_mandate_details: Option, storage_scheme: MerchantStorageScheme, ) -> errors::CustomResult<(), errors::VaultError> { let pm_update = payment_method::PaymentMethodUpdate::ConnectorMandateDetailsUpdate { @@ -2841,11 +2842,27 @@ pub async fn update_payment_method_connector_mandate_details( key_store: &domain::MerchantKeyStore, db: &dyn db::StorageInterface, pm: domain::PaymentMethod, - connector_mandate_details: Option, + connector_mandate_details: Option, storage_scheme: MerchantStorageScheme, ) -> errors::CustomResult<(), errors::VaultError> { + let mut connector_mandate_details_value = None; + if let Some(common_mandate) = connector_mandate_details { + let mut payments = serde_json::to_value(common_mandate.payments.as_ref()) + .change_context(errors::VaultError::UpdateInPaymentMethodDataTableFailed)?; + + let payouts = serde_json::to_value(common_mandate.payouts.as_ref()) + .change_context(errors::VaultError::UpdateInPaymentMethodDataTableFailed)?; + + if let Some(payments_object) = payments.as_object_mut() { + if !payouts.is_null() { + payments_object.insert("payouts".to_string(), payouts); + } + } + connector_mandate_details_value = Some(payments) + } + let pm_update = payment_method::PaymentMethodUpdate::ConnectorMandateDetailsUpdate { - connector_mandate_details, + connector_mandate_details: connector_mandate_details_value, }; db.update_payment_method(&(state.into()), key_store, pm, pm_update, storage_scheme) @@ -4936,14 +4953,7 @@ pub async fn list_customer_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to decrypt payment method billing address details")?; let connector_mandate_details = pm - .connector_mandate_details - .clone() - .map(|val| { - val.parse_value::( - "PaymentsMandateReference", - ) - }) - .transpose() + .get_common_mandate_reference() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; let mca_enabled = get_mca_status( @@ -4952,7 +4962,7 @@ pub async fn list_customer_payment_method( profile_id.clone(), merchant_account.get_id(), is_connector_agnostic_mit_enabled, - connector_mandate_details, + Some(connector_mandate_details), pm.network_transaction_id.as_ref(), ) .await?; @@ -5063,7 +5073,7 @@ pub async fn list_customer_payment_method( not(feature = "payment_methods_v2"), not(feature = "customer_v2") ))] -async fn get_pm_list_context( +pub async fn get_pm_list_context( state: &routes::SessionState, payment_method: &enums::PaymentMethod, #[cfg(feature = "payouts")] key_store: &domain::MerchantKeyStore, @@ -5218,7 +5228,7 @@ pub async fn get_mca_status( profile_id: Option, merchant_id: &id_type::MerchantId, is_connector_agnostic_mit_enabled: bool, - connector_mandate_details: Option, + connector_mandate_details: Option, network_transaction_id: Option<&String>, ) -> errors::RouterResult { if is_connector_agnostic_mit_enabled && network_transaction_id.is_some() { @@ -5256,7 +5266,7 @@ pub async fn get_mca_status( profile_id: Option, merchant_id: &id_type::MerchantId, is_connector_agnostic_mit_enabled: bool, - connector_mandate_details: Option<&payment_method::PaymentsMandateReference>, + connector_mandate_details: Option<&payment_method::CommonMandateReference>, network_transaction_id: Option<&String>, merchant_connector_accounts: &domain::MerchantConnectorAccounts, ) -> bool { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index cbf2cdea5384..c062118330ee 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -5778,16 +5778,12 @@ pub async fn decide_connector_for_normal_or_recurring_payment( where D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, { - let connector_mandate_details = &payment_method_info - .connector_mandate_details - .clone() - .map(|details| { - details - .parse_value::("connector_mandate_details") - }) - .transpose() + let connector_common_mandate_details = payment_method_info + .get_common_mandate_reference() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize connector mandate details")?; + .attach_printable("Failed to get the common mandate reference")?; + + let connector_mandate_details = connector_common_mandate_details.payments; let mut connector_choice = None; diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index b2c90e03b589..09b29e014fc5 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -6169,7 +6169,7 @@ where pub async fn validate_merchant_connector_ids_in_connector_mandate_details( state: &SessionState, key_store: &domain::MerchantKeyStore, - connector_mandate_details: &api_models::payment_methods::PaymentsMandateReference, + connector_mandate_details: &api_models::payment_methods::CommonMandateReference, merchant_id: &id_type::MerchantId, card_network: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { @@ -6197,45 +6197,46 @@ pub async fn validate_merchant_connector_ids_in_connector_mandate_details( }) .collect(); - for (migrating_merchant_connector_id, migrating_connector_mandate_details) in - connector_mandate_details.0.clone() - { - match ( - card_network.clone(), - merchant_connector_account_details_hash_map.get(&migrating_merchant_connector_id), - ) { - (Some(enums::CardNetwork::Discover), Some(merchant_connector_account_details)) => { - if let ("cybersource", None) = ( - merchant_connector_account_details.connector_name.as_str(), - migrating_connector_mandate_details - .original_payment_authorized_amount - .zip( - migrating_connector_mandate_details - .original_payment_authorized_currency, - ), - ) { - Err(errors::ApiErrorResponse::MissingRequiredFields { - field_names: vec![ - "original_payment_authorized_currency", - "original_payment_authorized_amount", - ], - }) - .attach_printable(format!( - "Invalid connector_mandate_details provided for connector {:?}", - migrating_merchant_connector_id - ))? + if let Some(payment_mandate_reference) = &connector_mandate_details.payments { + let payments_map = payment_mandate_reference.0.clone(); + for (migrating_merchant_connector_id, migrating_connector_mandate_details) in payments_map { + match ( + card_network.clone(), + merchant_connector_account_details_hash_map.get(&migrating_merchant_connector_id), + ) { + (Some(enums::CardNetwork::Discover), Some(merchant_connector_account_details)) => { + if let ("cybersource", None) = ( + merchant_connector_account_details.connector_name.as_str(), + migrating_connector_mandate_details + .original_payment_authorized_amount + .zip( + migrating_connector_mandate_details + .original_payment_authorized_currency, + ), + ) { + Err(errors::ApiErrorResponse::MissingRequiredFields { + field_names: vec![ + "original_payment_authorized_currency", + "original_payment_authorized_amount", + ], + }) + .attach_printable(format!( + "Invalid connector_mandate_details provided for connector {:?}", + migrating_merchant_connector_id + ))? + } } + (_, Some(_)) => (), + (_, None) => Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_connector_id", + }) + .attach_printable_lazy(|| { + format!( + "{:?} invalid merchant connector id in connector_mandate_details", + migrating_merchant_connector_id + ) + })?, } - (_, Some(_)) => (), - (_, None) => Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "merchant_connector_id", - }) - .attach_printable_lazy(|| { - format!( - "{:?} invalid merchant connector id in connector_mandate_details", - migrating_merchant_connector_id - ) - })?, } } Ok(()) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 91fdfae63fae..3a1e35a218fa 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -6,7 +6,7 @@ use api_models::routing::RoutableConnectorChoice; use async_trait::async_trait; use common_enums::{AuthorizationStatus, SessionUpdateStatus}; use common_utils::{ - ext_traits::{AsyncExt, Encode, ValueExt}, + ext_traits::{AsyncExt, Encode}, types::{keymanager::KeyManagerState, ConnectorTransactionId, MinorUnit}, }; use error_stack::{report, ResultExt}; @@ -1588,14 +1588,7 @@ async fn payment_response_update_tracker( { // Parse value to check for mandates' existence let mandate_details = payment_method - .connector_mandate_details - .clone() - .map(|val| { - val.parse_value::( - "PaymentsMandateReference", - ) - }) - .transpose() + .get_common_mandate_reference() .change_context( errors::ApiErrorResponse::InternalServerError, ) @@ -1607,15 +1600,11 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.merchant_connector_id.clone() { // check if the mandate has not already been set to active - if !mandate_details - .as_ref() - .map(|payment_mandate_reference| { - - payment_mandate_reference.0.get(&mca_id) + if !mandate_details.payments + .as_ref() + .and_then(|payments| payments.0.get(&mca_id)) .map(|payment_mandate_reference_record| payment_mandate_reference_record.connector_mandate_status == Some(common_enums::ConnectorMandateStatus::Active)) .unwrap_or(false) - }) - .unwrap_or(false) { let (connector_mandate_id, mandate_metadata,connector_mandate_request_reference_id) = payment_data.payment_attempt.connector_mandate_detail.clone() @@ -1624,7 +1613,7 @@ async fn payment_response_update_tracker( // Update the connector mandate details with the payment attempt connector mandate id let connector_mandate_details = tokenization::update_connector_mandate_details( - mandate_details, + Some(mandate_details), payment_data.payment_attempt.payment_method_type, Some( payment_data diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 0a57819816fb..d0ddcd4427a4 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -152,7 +152,7 @@ pub fn make_dsl_input_for_payouts( .map(api_enums::PaymentMethod::foreign_from), payment_method_type: payout_data .payout_method_data - .clone() + .as_ref() .map(api_enums::PaymentMethodType::foreign_from), card_network: None, }; diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 94f221dd722f..4e4db44ca9f4 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -1173,7 +1173,7 @@ pub fn add_connector_mandate_details_in_payment_method( connector_mandate_id: Option, mandate_metadata: Option>, connector_mandate_request_reference_id: Option, -) -> Option { +) -> Option { let mut mandate_details = HashMap::new(); if let Some((mca_id, connector_mandate_id)) = @@ -1191,7 +1191,10 @@ pub fn add_connector_mandate_details_in_payment_method( connector_mandate_request_reference_id, }, ); - Some(diesel_models::PaymentsMandateReference(mandate_details)) + Some(diesel_models::CommonMandateReference { + payments: Some(diesel_models::PaymentsMandateReference(mandate_details)), + payouts: None, + }) } else { None } @@ -1200,7 +1203,7 @@ pub fn add_connector_mandate_details_in_payment_method( #[allow(clippy::too_many_arguments)] #[cfg(feature = "v1")] pub fn update_connector_mandate_details( - mandate_details: Option, + mandate_details: Option, payment_method_type: Option, authorized_amount: Option, authorized_currency: Option, @@ -1208,8 +1211,11 @@ pub fn update_connector_mandate_details( connector_mandate_id: Option, mandate_metadata: Option>, connector_mandate_request_reference_id: Option, -) -> RouterResult> { - let mandate_reference = match mandate_details { +) -> RouterResult> { + let mandate_reference = match mandate_details + .as_ref() + .and_then(|common_mandate| common_mandate.payments.clone()) + { Some(mut payment_mandate_reference) => { if let Some((mca_id, connector_mandate_id)) = merchant_connector_id.clone().zip(connector_mandate_id) @@ -1237,7 +1243,13 @@ pub fn update_connector_mandate_details( connector_mandate_status: Some(ConnectorMandateStatus::Active), connector_mandate_request_reference_id, }); - Some(payment_mandate_reference) + + let payout_data = mandate_details.and_then(|common_mandate| common_mandate.payouts); + + Some(diesel_models::CommonMandateReference { + payments: Some(payment_mandate_reference), + payouts: payout_data, + }) } else { None } @@ -1252,11 +1264,5 @@ pub fn update_connector_mandate_details( connector_mandate_request_reference_id, ), }; - let connector_mandate_details = mandate_reference - .map(|mand| mand.encode_to_value()) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to serialize customer acceptance to value")?; - - Ok(connector_mandate_details) + Ok(mandate_reference) } diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index b2d6b2ace45c..81e656623b84 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -4,7 +4,10 @@ pub mod helpers; pub mod retry; pub mod transformers; pub mod validator; -use std::{collections::HashSet, vec::IntoIter}; +use std::{ + collections::{HashMap, HashSet}, + vec::IntoIter, +}; #[cfg(feature = "olap")] use api_models::payments as payment_enums; @@ -21,10 +24,16 @@ use common_utils::{ use diesel_models::{ enums as storage_enums, generic_link::{GenericLinkNew, PayoutLink}, + CommonMandateReference, PayoutsMandateReference, PayoutsMandateReferenceRecord, +}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use diesel_models::{ + PaymentsMandateReference, PaymentsMandateReferenceRecord as PaymentsMandateReferenceRecordV2, }; use error_stack::{report, ResultExt}; #[cfg(feature = "olap")] use futures::future::join_all; +use hyperswitch_domain_models::payment_methods::PaymentMethod; use masking::{PeekInterface, Secret}; #[cfg(feature = "payout_retry")] use retry::GsmValidation; @@ -72,6 +81,7 @@ pub struct PayoutData { pub should_terminate: bool, pub payout_link: Option, pub current_locale: String, + pub payment_method: Option, } // ********************************************** CORE FLOWS ********************************************** @@ -320,7 +330,7 @@ pub async fn payouts_create_core( locale: &str, ) -> RouterResponse { // Validate create request - let (payout_id, payout_method_data, profile_id, customer) = + let (payout_id, payout_method_data, profile_id, customer, payment_method) = validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; // Create DB entries @@ -334,6 +344,7 @@ pub async fn payouts_create_core( payout_method_data.as_ref(), locale, customer.as_ref(), + payment_method.clone(), ) .await?; @@ -1136,12 +1147,13 @@ pub async fn call_connector_payout( ) .await?; // Create customer's disbursement account flow - complete_create_recipient_disburse_account( + Box::pin(complete_create_recipient_disburse_account( state, merchant_account, connector_data, payout_data, - ) + key_store, + )) .await?; // Payout creation flow Box::pin(complete_create_payout( @@ -1353,6 +1365,39 @@ pub async fn create_recipient( // Helps callee functions skip the execution payout_data.should_terminate = true; + } else if let Some(status) = recipient_create_data.status { + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: payout_data + .payout_attempt + .connector_payout_id + .to_owned(), + status, + error_code: None, + error_message: None, + is_eligible: recipient_create_data.payout_eligible, + unified_code: None, + unified_message: None, + }; + payout_data.payout_attempt = db + .update_payout_attempt( + &payout_data.payout_attempt, + updated_payout_attempt, + &payout_data.payouts, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating payout_attempt in db")?; + payout_data.payouts = db + .update_payout( + &payout_data.payouts, + storage::PayoutsUpdate::StatusUpdate { status }, + &payout_data.payout_attempt, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating payouts in db")?; } } Err(err) => Err(errors::ApiErrorResponse::PayoutFailed { @@ -1952,17 +1997,29 @@ pub async fn complete_create_recipient_disburse_account( merchant_account: &domain::MerchantAccount, connector_data: &api::ConnectorData, payout_data: &mut PayoutData, + key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { if !payout_data.should_terminate - && payout_data.payout_attempt.status - == storage_enums::PayoutStatus::RequiresVendorAccountCreation + && matches!( + payout_data.payout_attempt.status, + storage_enums::PayoutStatus::RequiresVendorAccountCreation + | storage_enums::PayoutStatus::RequiresCreation + ) && connector_data .connector_name .supports_vendor_disburse_account_create_for_payout() + && helpers::should_create_connector_transfer_method(&*payout_data, connector_data)? + .is_none() { - create_recipient_disburse_account(state, merchant_account, connector_data, payout_data) - .await - .attach_printable("Creation of customer failed")?; + Box::pin(create_recipient_disburse_account( + state, + merchant_account, + connector_data, + payout_data, + key_store, + )) + .await + .attach_printable("Creation of customer failed")?; } Ok(()) } @@ -1972,6 +2029,7 @@ pub async fn create_recipient_disburse_account( merchant_account: &domain::MerchantAccount, connector_data: &api::ConnectorData, payout_data: &mut PayoutData, + key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { // 1. Form Router data let router_data = @@ -2005,7 +2063,7 @@ pub async fn create_recipient_disburse_account( .status .unwrap_or(payout_attempt.status.to_owned()); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_response_data.connector_payout_id, + connector_payout_id: payout_response_data.connector_payout_id.clone(), status, error_code: None, error_message: None, @@ -2023,6 +2081,82 @@ pub async fn create_recipient_disburse_account( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payout_attempt in db")?; + + if let ( + true, + Some(ref payout_method_data), + Some(connector_payout_id), + Some(customer_details), + Some(merchant_connector_id), + ) = ( + payout_data.payouts.recurring, + payout_data.payout_method_data.clone(), + payout_response_data.connector_payout_id.clone(), + payout_data.customer_details.clone(), + connector_data.merchant_connector_id.clone(), + ) { + let connector_mandate_details = HashMap::from([( + merchant_connector_id.clone(), + PayoutsMandateReferenceRecord { + transfer_method_id: Some(connector_payout_id), + }, + )]); + + let common_connector_mandate = CommonMandateReference { + payments: None, + payouts: Some(PayoutsMandateReference(connector_mandate_details)), + }; + + let connector_mandate_details_value = + serde_json::to_value(common_connector_mandate.clone()).ok(); + + if let Some(pm_method) = payout_data.payment_method.clone() { + let pm_update = + diesel_models::PaymentMethodUpdate::ConnectorMandateDetailsUpdate { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + connector_mandate_details: connector_mandate_details_value, + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + connector_mandate_details: Some(common_connector_mandate), + }; + + payout_data.payment_method = Some( + db.update_payment_method( + &(state.into()), + key_store, + pm_method, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?, + ); + } else { + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] + let customer_id = Some(customer_details.customer_id); + + #[cfg(all(feature = "v2", feature = "customer_v2"))] + let customer_id = customer_details.merchant_reference_id; + + if let Some(customer_id) = customer_id { + helpers::save_payout_data_to_locker( + state, + payout_data, + &customer_id, + payout_method_data, + connector_mandate_details_value, + merchant_account, + key_store, + ) + .await + .attach_printable("Failed to save payout data to locker")?; + } + }; + } } Err(err) => { let (error_code, error_message) = (Some(err.code), Some(err.message)); @@ -2293,6 +2427,7 @@ pub async fn fulfill_payout( payout_data, &customer_id, &payout_method_data, + None, merchant_account, key_store, ) @@ -2367,6 +2502,22 @@ pub async fn response_handler( ) -> RouterResponse { let payout_attempt = payout_data.payout_attempt.to_owned(); let payouts = payout_data.payouts.to_owned(); + + let payout_method_id: Option = payout_data.payment_method.as_ref().map(|pm| { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + { + pm.payment_method_id.clone() + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + { + pm.id.clone().get_string_repr().to_string() + } + }); + let payout_link = payout_data.payout_link.to_owned(); let billing_address = payout_data.billing_address.to_owned(); let customer_details = payout_data.customer_details.to_owned(); @@ -2438,6 +2589,7 @@ pub async fn response_handler( .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to parse payout link's URL")?, + payout_method_id, }; Ok(services::ApplicationResponse::Json(response)) } @@ -2454,6 +2606,7 @@ pub async fn payout_create_db_entries( _stored_payout_method_data: Option<&payouts::PayoutMethodData>, _locale: &str, _customer: Option<&domain::Customer>, + _payment_method: Option, ) -> RouterResult { todo!() } @@ -2471,6 +2624,7 @@ pub async fn payout_create_db_entries( stored_payout_method_data: Option<&payouts::PayoutMethodData>, locale: &str, customer: Option<&domain::Customer>, + payment_method: Option, ) -> RouterResult { let db = &*state.store; let merchant_id = merchant_account.get_id(); @@ -2523,13 +2677,22 @@ pub async fn payout_create_db_entries( // Make payouts entry let currency = req.currency.to_owned().get_required_value("currency")?; - let payout_type = req.payout_type.to_owned(); - let payout_method_id = if stored_payout_method_data.is_some() { - req.payout_token.to_owned() - } else { - None + let (payout_method_id, payout_type) = match stored_payout_method_data { + Some(payout_method_data) => ( + payment_method + .as_ref() + .map(|pm| pm.payment_method_id.clone()), + Some(api_enums::PayoutType::foreign_from(payout_method_data)), + ), + None => ( + payment_method + .as_ref() + .map(|pm| pm.payment_method_id.clone()), + req.payout_type.to_owned(), + ), }; + let client_secret = utils::generate_id( consts::ID_LENGTH, format!("payout_{payout_id}_secret").as_str(), @@ -2648,6 +2811,7 @@ pub async fn payout_create_db_entries( profile_id: profile_id.to_owned(), payout_link, current_locale: locale.to_string(), + payment_method, }) } @@ -2837,6 +3001,23 @@ pub async fn make_payout_data( .await .transpose()?; + let payout_method_id = payouts.payout_method_id.clone(); + let mut payment_method: Option = None; + + if let Some(pm_id) = payout_method_id { + payment_method = Some( + 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")?, + ); + } + Ok(PayoutData { billing_address, business_profile, @@ -2849,6 +3030,7 @@ pub async fn make_payout_data( profile_id, payout_link, current_locale: locale.to_string(), + payment_method, }) } diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index b07094c321cc..938a8edd5082 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -194,6 +194,43 @@ pub async fn make_payout_method_data<'a>( } } +pub fn should_create_connector_transfer_method( + payout_data: &PayoutData, + connector_data: &api::ConnectorData, +) -> RouterResult> { + let connector_transfer_method_id = if let Some(pm) = &payout_data.payment_method { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + let common_mandate_reference = pm + .get_common_mandate_reference() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize connector mandate details")?; + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + let common_mandate_reference = pm.connector_mandate_details.clone().unwrap_or_default(); + + if let Some(merchant_connector_id) = connector_data.merchant_connector_id.as_ref() { + common_mandate_reference + .payouts + .and_then(|payouts_mandate_reference| { + payouts_mandate_reference.get(merchant_connector_id).map( + |payouts_mandate_reference_record| { + payouts_mandate_reference_record.transfer_method_id.clone() + }, + ) + }) + .flatten() + } else { + None + } + } else { + None + }; + Ok(connector_transfer_method_id) +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -203,6 +240,7 @@ pub async fn save_payout_data_to_locker( payout_data: &mut PayoutData, customer_id: &id_type::CustomerId, payout_method_data: &api::PayoutMethodData, + connector_mandate_details: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { @@ -290,14 +328,14 @@ pub async fn save_payout_data_to_locker( None, Some(bank.to_owned()), None, - api_enums::PaymentMethodType::foreign_from(bank.to_owned()), + api_enums::PaymentMethodType::foreign_from(bank), ), payouts::PayoutMethodData::Wallet(wallet) => ( payload, None, None, Some(wallet.to_owned()), - api_enums::PaymentMethodType::foreign_from(wallet.to_owned()), + api_enums::PaymentMethodType::foreign_from(wallet), ), payouts::PayoutMethodData::Card(_) => { Err(errors::ApiErrorResponse::InternalServerError)? @@ -409,9 +447,7 @@ pub async fn save_payout_data_to_locker( let card_isin = card_details.as_ref().map(|c| c.card_number.get_card_isin()); let mut payment_method = api::PaymentMethodCreate { - payment_method: Some(api_enums::PaymentMethod::foreign_from( - payout_method_data.to_owned(), - )), + payment_method: Some(api_enums::PaymentMethod::foreign_from(payout_method_data)), payment_method_type: Some(payment_method_type), payment_method_issuer: None, payment_method_issuer_code: None, @@ -497,7 +533,7 @@ pub async fn save_payout_data_to_locker( None, api::PaymentMethodCreate { payment_method: Some(api_enums::PaymentMethod::foreign_from( - payout_method_data.to_owned(), + payout_method_data, )), payment_method_type: Some(payment_method_type), payment_method_issuer: None, @@ -520,28 +556,30 @@ pub async fn save_payout_data_to_locker( // Insert new entry in payment_methods table if should_insert_in_pm_table { let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); - cards::create_payment_method( - state, - &new_payment_method, - customer_id, - &payment_method_id, - Some(stored_resp.card_reference.clone()), - merchant_account.get_id(), - None, - None, - card_details_encrypted.clone().map(Into::into), - key_store, - None, - None, - None, - merchant_account.storage_scheme, - None, - None, - None, - None, - None, - ) - .await?; + payout_data.payment_method = Some( + cards::create_payment_method( + state, + &new_payment_method, + customer_id, + &payment_method_id, + Some(stored_resp.card_reference.clone()), + merchant_account.get_id(), + None, + None, + card_details_encrypted.clone().map(Into::into), + key_store, + connector_mandate_details, + None, + None, + merchant_account.storage_scheme, + None, + None, + None, + None, + None, + ) + .await?, + ); } /* 1. Delete from locker @@ -600,22 +638,28 @@ pub async fn save_payout_data_to_locker( let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data: card_details_encrypted.map(Into::into), }; - db.update_payment_method( - &(state.into()), - key_store, - existing_pm, - pm_update, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to add payment method in db")?; + payout_data.payment_method = Some( + db.update_payment_method( + &(state.into()), + key_store, + existing_pm, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?, + ); }; // Store card_reference in payouts table - let updated_payout = storage::PayoutsUpdate::PayoutMethodIdUpdate { - payout_method_id: stored_resp.card_reference.to_owned(), + let payout_method_id = match &payout_data.payment_method { + Some(pm) => pm.payment_method_id.clone(), + None => stored_resp.card_reference.to_owned(), }; + + let updated_payout = storage::PayoutsUpdate::PayoutMethodIdUpdate { payout_method_id }; + payout_data.payouts = db .update_payout( payouts, @@ -636,6 +680,7 @@ pub async fn save_payout_data_to_locker( _payout_data: &mut PayoutData, _customer_id: &id_type::CustomerId, _payout_method_data: &api::PayoutMethodData, + _connector_mandate_details: Option, _merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { diff --git a/crates/router/src/core/payouts/transformers.rs b/crates/router/src/core/payouts/transformers.rs index c1f314043053..c1a6c6cc23ce 100644 --- a/crates/router/src/core/payouts/transformers.rs +++ b/crates/router/src/core/payouts/transformers.rs @@ -115,6 +115,7 @@ impl phone_country_code: customer .as_ref() .and_then(|customer| customer.phone_country_code.clone()), + payout_method_id: payout.payout_method_id, } } } diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index c647d6eaf102..3d7d457635cf 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -7,10 +7,17 @@ use common_utils::validation::validate_domain_against_allowed_domains; use diesel_models::generic_link::PayoutLink; use error_stack::{report, ResultExt}; pub use hyperswitch_domain_models::errors::StorageError; +use hyperswitch_domain_models::payment_methods::PaymentMethod; use router_env::{instrument, tracing, which as router_env_which, Env}; use url::Url; use super::helpers; +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2"), + not(feature = "customer_v2") +))] +use crate::core::payment_methods::cards::get_pm_list_context; use crate::{ core::{ errors::{self, RouterResult}, @@ -20,6 +27,7 @@ use crate::{ routes::SessionState, types::{api::payouts, domain, storage}, utils, + utils::OptionExt, }; #[instrument(skip(db))] @@ -57,6 +65,7 @@ pub async fn validate_create_request( Option, String, Option, + Option, )> { todo!() } @@ -76,6 +85,7 @@ pub async fn validate_create_request( Option, common_utils::id_type::ProfileId, Option, + Option, )> { let merchant_id = merchant_account.get_id(); @@ -137,28 +147,6 @@ pub async fn validate_create_request( None }; - // payout_token - let payout_method_data = match (req.payout_token.as_ref(), customer.as_ref()) { - (Some(_), None) => Err(report!(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer or customer_id when payout_token is provided" - })), - (Some(payout_token), Some(customer)) => { - helpers::make_payout_method_data( - state, - req.payout_method_data.as_ref(), - Some(payout_token), - &customer.customer_id, - merchant_account.get_id(), - req.payout_type, - merchant_key_store, - None, - merchant_account.storage_scheme, - ) - .await - } - _ => Ok(None), - }?; - #[cfg(feature = "v1")] let profile_id = core_utils::get_profile_id_from_business_details( &state.into(), @@ -182,7 +170,104 @@ pub async fn validate_create_request( }) .attach_printable("Profile id is a mandatory parameter")?; - Ok((payout_id, payout_method_data, profile_id, customer)) + let payment_method: Option = + match (req.payout_token.as_ref(), req.payout_method_id.clone()) { + (Some(_), Some(_)) => Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Only one of payout_method_id or payout_token should be provided." + .to_string(), + })), + (None, Some(payment_method_id)) => match customer.as_ref() { + Some(customer) => { + let payment_method = db + .find_payment_method( + &state.into(), + merchant_key_store, + &payment_method_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?; + + utils::when(payment_method.customer_id != customer.customer_id, || { + Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment method does not belong to this customer_id".to_string(), + }) + .attach_printable( + "customer_id in payment_method does not match with customer_id in request", + )) + })?; + Ok(Some(payment_method)) + } + None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer_id when payment_method_id is passed", + })), + }, + _ => Ok(None), + }?; + + // payout_token + let payout_method_data = match ( + req.payout_token.as_ref(), + customer.as_ref(), + payment_method.as_ref(), + ) { + (Some(_), None, _) => Err(report!(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer or customer_id when payout_token is provided" + })), + (Some(payout_token), Some(customer), _) => { + helpers::make_payout_method_data( + state, + req.payout_method_data.as_ref(), + Some(payout_token), + &customer.customer_id, + merchant_account.get_id(), + req.payout_type, + merchant_key_store, + None, + merchant_account.storage_scheme, + ) + .await + } + (_, Some(_), Some(payment_method)) => { + match get_pm_list_context( + state, + payment_method + .payment_method + .as_ref() + .get_required_value("payment_method_id")?, + merchant_key_store, + payment_method, + None, + false, + ) + .await? + { + Some(pm) => match (pm.card_details, pm.bank_transfer_details) { + (Some(card), _) => Ok(Some(payouts::PayoutMethodData::Card( + api_models::payouts::CardPayout { + card_number: card.card_number.get_required_value("card_number")?, + card_holder_name: card.card_holder_name, + expiry_month: card.expiry_month.get_required_value("expiry_month")?, + expiry_year: card.expiry_year.get_required_value("expiry_month")?, + }, + ))), + (_, Some(bank)) => Ok(Some(payouts::PayoutMethodData::Bank(bank))), + _ => Ok(None), + }, + None => Ok(None), + } + } + _ => Ok(None), + }?; + + Ok(( + payout_id, + payout_method_data, + profile_id, + customer, + payment_method, + )) } pub fn validate_payout_link_request( diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index c441e32581e8..ac525e24009d 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -24,7 +24,7 @@ use uuid::Uuid; use super::payments::helpers; #[cfg(feature = "payouts")] -use super::payouts::PayoutData; +use super::payouts::{helpers as payout_helpers, PayoutData}; #[cfg(feature = "payouts")] use crate::core::payments; use crate::{ @@ -148,6 +148,9 @@ pub async fn construct_payout_router_data<'a, F>( _ => None, }; + let connector_transfer_method_id = + payout_helpers::should_create_connector_transfer_method(&*payout_data, connector_data)?; + let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().to_owned(), @@ -189,6 +192,7 @@ pub async fn construct_payout_router_data<'a, F>( phone: c.phone.map(Encryptable::into_inner), phone_country_code: c.phone_country_code, }), + connector_transfer_method_id, }, response: Ok(types::PayoutsResponseData::default()), access_token: None, diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index ff9849958b51..7188a8c21fe1 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -4,7 +4,7 @@ use actix_web::FromRequest; #[cfg(feature = "payouts")] use api_models::payouts as payout_models; use api_models::webhooks::{self, WebhookResponseTracker}; -use common_utils::{errors::ReportSwitchExt, events::ApiEventsType, ext_traits::ValueExt}; +use common_utils::{errors::ReportSwitchExt, events::ApiEventsType}; use diesel_models::ConnectorMandateReferenceId; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ @@ -1843,14 +1843,7 @@ async fn update_connector_mandate_details( let updated_connector_mandate_details = if let Some(webhook_mandate_details) = webhook_connector_mandate_details { let mandate_details = payment_method_info - .connector_mandate_details - .clone() - .map(|val| { - val.parse_value::( - "PaymentsMandateReference", - ) - }) - .transpose() + .get_common_mandate_reference() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to deserialize to Payment Mandate Reference")?; @@ -1859,13 +1852,9 @@ async fn update_connector_mandate_details( .clone() .get_required_value("merchant_connector_id")?; - if mandate_details - .as_ref() - .map(|details: &diesel_models::PaymentsMandateReference| { - !details.0.contains_key(&merchant_connector_account_id) - }) - .unwrap_or(true) - { + if mandate_details.payments.as_ref().map_or(true, |payments| { + !payments.0.contains_key(&merchant_connector_account_id) + }) { // Update the payment attempt to maintain consistency across tables. let (mandate_metadata, connector_mandate_request_reference_id) = payment_attempt @@ -1910,7 +1899,7 @@ async fn update_connector_mandate_details( insert_mandate_details( &payment_attempt, &webhook_mandate_details, - mandate_details, + Some(mandate_details), )? } else { logger::info!( @@ -1922,8 +1911,24 @@ async fn update_connector_mandate_details( None }; + let mut connector_mandate_details_value = None; + if let Some(common_mandate) = updated_connector_mandate_details { + let mut payments = serde_json::to_value(common_mandate.payments.as_ref()) + .change_context(errors::ApiErrorResponse::MandateUpdateFailed)?; + + let payouts = serde_json::to_value(common_mandate.payouts.as_ref()) + .change_context(errors::ApiErrorResponse::MandateUpdateFailed)?; + + if let Some(payments_object) = payments.as_object_mut() { + if !payouts.is_null() { + payments_object.insert("payouts".to_string(), payouts); + } + } + connector_mandate_details_value = Some(payments) + } + let pm_update = diesel_models::PaymentMethodUpdate::ConnectorNetworkTransactionIdAndMandateDetailsUpdate { - connector_mandate_details: updated_connector_mandate_details.map(masking::Secret::new), + connector_mandate_details: connector_mandate_details_value.map(masking::Secret::new), network_transaction_id: webhook_connector_network_transaction_id .map(|webhook_network_transaction_id| webhook_network_transaction_id.get_id().clone()), }; @@ -1948,8 +1953,8 @@ async fn update_connector_mandate_details( fn insert_mandate_details( payment_attempt: &PaymentAttempt, webhook_mandate_details: &hyperswitch_domain_models::router_flow_types::ConnectorMandateDetails, - payment_method_mandate_details: Option, -) -> CustomResult, errors::ApiErrorResponse> { + payment_method_mandate_details: Option, +) -> CustomResult, errors::ApiErrorResponse> { let (mandate_metadata, connector_mandate_request_reference_id) = payment_attempt .connector_mandate_detail .clone() diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index bbcdfc535df8..b458bbf8f745 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1386,8 +1386,8 @@ impl ForeignFrom for payments::CaptureResponse { } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethodType { - fn foreign_from(value: api_models::payouts::PayoutMethodData) -> Self { +impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_enums::PaymentMethodType { + fn foreign_from(value: &api_models::payouts::PayoutMethodData) -> Self { match value { api_models::payouts::PayoutMethodData::Bank(bank) => Self::foreign_from(bank), api_models::payouts::PayoutMethodData::Card(_) => Self::Debit, @@ -1397,8 +1397,8 @@ impl ForeignFrom for api_enums::PaymentMe } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethodType { - fn foreign_from(value: api_models::payouts::Bank) -> Self { +impl ForeignFrom<&api_models::payouts::Bank> for api_enums::PaymentMethodType { + fn foreign_from(value: &api_models::payouts::Bank) -> Self { match value { api_models::payouts::Bank::Ach(_) => Self::Ach, api_models::payouts::Bank::Bacs(_) => Self::Bacs, @@ -1409,8 +1409,8 @@ impl ForeignFrom for api_enums::PaymentMethodType { } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethodType { - fn foreign_from(value: api_models::payouts::Wallet) -> Self { +impl ForeignFrom<&api_models::payouts::Wallet> for api_enums::PaymentMethodType { + fn foreign_from(value: &api_models::payouts::Wallet) -> Self { match value { api_models::payouts::Wallet::Paypal(_) => Self::Paypal, api_models::payouts::Wallet::Venmo(_) => Self::Venmo, @@ -1419,8 +1419,8 @@ impl ForeignFrom for api_enums::PaymentMethodType { } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethod { - fn foreign_from(value: api_models::payouts::PayoutMethodData) -> Self { +impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_enums::PaymentMethod { + fn foreign_from(value: &api_models::payouts::PayoutMethodData) -> Self { match value { api_models::payouts::PayoutMethodData::Bank(_) => Self::BankTransfer, api_models::payouts::PayoutMethodData::Card(_) => Self::Card, @@ -1429,6 +1429,17 @@ impl ForeignFrom for api_enums::PaymentMe } } +#[cfg(feature = "payouts")] +impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_models::enums::PayoutType { + fn foreign_from(value: &api_models::payouts::PayoutMethodData) -> Self { + match value { + api_models::payouts::PayoutMethodData::Bank(_) => Self::Bank, + api_models::payouts::PayoutMethodData::Card(_) => Self::Card, + api_models::payouts::PayoutMethodData::Wallet(_) => Self::Wallet, + } + } +} + #[cfg(feature = "payouts")] impl ForeignFrom for api_enums::PaymentMethod { fn foreign_from(value: api_models::enums::PayoutType) -> Self { diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 305b11fe5b3d..7dffce035b25 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -471,6 +471,7 @@ pub trait ConnectorActions: Connector { }), vendor_details: None, priority: None, + connector_transfer_method_id: None, }, payment_info, )