diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c55ef48328..651b009656 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -430,6 +430,40 @@ pub struct PaymentsUpdateIntentRequest { pub set_active_attempt_id: Option, } +#[cfg(feature = "v2")] +impl PaymentsUpdateIntentRequest { + pub fn update_feature_metadata_and_active_attempt_with_api( + feature_metadata: FeatureMetadata, + set_active_attempt_id: api_enums::UpdateActiveAttempt, + ) -> Self { + Self { + feature_metadata: Some(feature_metadata), + set_active_attempt_id: Some(set_active_attempt_id), + amount_details: None, + routing_algorithm_id: None, + capture_method: None, + authentication_type: None, + billing: None, + shipping: None, + customer_present: None, + description: None, + return_url: None, + setup_future_usage: None, + apply_mit_exemption: None, + statement_descriptor: None, + order_details: None, + allowed_payment_method_types: None, + metadata: None, + connector_metadata: None, + payment_link_config: None, + request_incremental_authorization: None, + session_expiry: None, + frm_metadata: None, + request_external_three_ds_authentication: None, + } + } +} + #[derive(Debug, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] #[cfg(feature = "v2")] @@ -7459,7 +7493,7 @@ pub struct PaymentsStartRequest { /// additional data that might be required by hyperswitch #[cfg(feature = "v2")] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize, ToSchema)] pub struct FeatureMetadata { /// Redirection response coming in request as metadata field only for redirection scenarios #[schema(value_type = Option)] @@ -7480,6 +7514,18 @@ impl FeatureMetadata { .as_ref() .map(|metadata| metadata.total_retry_count) } + + pub fn set_payment_revenue_recovery_metadata_using_api( + self, + payment_revenue_recovery_metadata: PaymentRevenueRecoveryMetadata, + ) -> Self { + Self { + redirect_response: self.redirect_response, + search_tags: self.search_tags, + apple_pay_recurring_details: self.apple_pay_recurring_details, + payment_revenue_recovery_metadata: Some(payment_revenue_recovery_metadata), + } + } } /// additional data that might be required by hyperswitch @@ -8391,6 +8437,28 @@ pub struct PaymentRevenueRecoveryMetadata { #[schema(value_type = Connector, example = "stripe")] pub connector: common_enums::connector_enums::Connector, } +#[cfg(feature = "v2")] +impl PaymentRevenueRecoveryMetadata { + pub fn set_payment_transmission_field_for_api_request( + &mut self, + payment_connector_transmission: PaymentConnectorTransmission, + ) { + self.payment_connector_transmission = payment_connector_transmission; + } + pub fn get_payment_token_for_api_request(&self) -> ProcessorPaymentToken { + ProcessorPaymentToken { + processor_payment_token: self + .billing_connector_payment_details + .payment_processor_token + .clone(), + merchant_connector_id: Some(self.active_attempt_payment_connector_id.clone()), + } + } + pub fn get_merchant_connector_id_for_api_request(&self) -> id_type::MerchantConnectorAccountId { + self.active_attempt_payment_connector_id.clone() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[cfg(feature = "v2")] pub struct BillingConnectorPaymentDetails { diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 296bdbd9dd..1add1f4f16 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -7608,7 +7608,7 @@ pub enum AdyenSplitType { #[serde(rename = "snake_case")] pub enum PaymentConnectorTransmission { /// Failed to call the payment connector - ConnectorCallFailed, + ConnectorCallUnsuccessful, /// Payment Connector call succeeded ConnectorCallSucceeded, } diff --git a/crates/diesel_models/src/types.rs b/crates/diesel_models/src/types.rs index b1fa219703..eb2818ba24 100644 --- a/crates/diesel_models/src/types.rs +++ b/crates/diesel_models/src/types.rs @@ -43,7 +43,7 @@ impl masking::SerializableSecret for OrderDetailsWithAmount {} common_utils::impl_to_sql_from_sql_json!(OrderDetailsWithAmount); #[cfg(feature = "v2")] -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, FromSqlRow, AsExpression)] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, FromSqlRow, AsExpression)] #[diesel(sql_type = Json)] pub struct FeatureMetadata { /// Redirection response coming in request as metadata field only for redirection scenarios diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 76e06ab9b4..1b83e845ed 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -573,6 +573,24 @@ impl From for payments::AmountDetails { } } } + +#[cfg(feature = "v2")] +impl From for api_models::payments::AmountDetailsSetter { + fn from(amount_details: payments::AmountDetails) -> Self { + Self { + order_amount: amount_details.order_amount.into(), + currency: amount_details.currency, + shipping_cost: amount_details.shipping_cost, + order_tax_amount: amount_details + .tax_details + .and_then(|tax_detail| tax_detail.get_default_tax_amount()), + skip_external_tax_calculation: amount_details.skip_external_tax_calculation, + skip_surcharge_calculation: amount_details.skip_surcharge_calculation, + surcharge_amount: amount_details.surcharge_amount, + tax_on_surcharge: amount_details.tax_on_surcharge, + } + } +} #[cfg(feature = "v2")] impl From<&api_models::payments::PaymentAttemptAmountDetails> for payments::payment_attempt::AttemptAmountDetailsSetter diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 7eb8a8091a..7f1278414e 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -484,12 +484,14 @@ impl if let Some(ref mut payment_revenue_recovery_metadata) = feature_metadata.payment_revenue_recovery_metadata { - payment_revenue_recovery_metadata.payment_connector_transmission = - if self.response.is_ok() { - common_enums::PaymentConnectorTransmission::ConnectorCallSucceeded - } else { - common_enums::PaymentConnectorTransmission::ConnectorCallFailed - }; + payment_revenue_recovery_metadata.payment_connector_transmission = if self + .response + .is_ok() + { + common_enums::PaymentConnectorTransmission::ConnectorCallSucceeded + } else { + common_enums::PaymentConnectorTransmission::ConnectorCallUnsuccessful + }; } Box::new(feature_metadata) }); @@ -1012,7 +1014,8 @@ impl attempt_status, connector_transaction_id, } = error_response.clone(); - let attempt_status = attempt_status.unwrap_or(self.status); + + let attempt_status = attempt_status.unwrap_or(common_enums::AttemptStatus::Failure); let error_details = ErrorDetails { code, diff --git a/crates/router/src/core/passive_churn_recovery.rs b/crates/router/src/core/passive_churn_recovery.rs index 3b4ca0d7af..eead6ad254 100644 --- a/crates/router/src/core/passive_churn_recovery.rs +++ b/crates/router/src/core/passive_churn_recovery.rs @@ -1,12 +1,14 @@ pub mod transformers; pub mod types; -use api_models::payments::PaymentsRetrieveRequest; -use common_utils::{self, id_type, types::keymanager::KeyManagerState}; +use api_models::payments::{PaymentRevenueRecoveryMetadata, PaymentsRetrieveRequest}; +use common_utils::{self, ext_traits::OptionExt, id_type, types::keymanager::KeyManagerState}; use diesel_models::process_tracker::business_status; use error_stack::{self, ResultExt}; use hyperswitch_domain_models::{ + behaviour::ReverseConversion, errors::api_error_response, payments::{PaymentIntent, PaymentStatusData}, + ApiModelToDieselModelConvertor, }; use scheduler::errors; @@ -35,34 +37,44 @@ pub async fn perform_execute_payment( payment_intent: &PaymentIntent, ) -> Result<(), errors::ProcessTrackerError> { let db = &*state.store; + + let mut pcr_metadata = payment_intent + .feature_metadata + .as_ref() + .and_then(|feature_metadata| feature_metadata.payment_revenue_recovery_metadata.clone()) + .get_required_value("Payment Revenue Recovery Metadata")? + .convert_back(); + let decision = pcr_types::Decision::get_decision_based_on_params( state, payment_intent.status, - false, + pcr_metadata.payment_connector_transmission, payment_intent.active_attempt_id.clone(), pcr_data, &tracking_data.global_payment_id, ) .await?; + // TODO decide if its a global failure or is it requeueable error match decision { pcr_types::Decision::Execute => { let action = pcr_types::Action::execute_payment( - db, + state, pcr_data.merchant_account.get_id(), payment_intent, execute_task_process, + pcr_data, + &pcr_metadata, ) .await?; - action - .execute_payment_task_response_handler( - db, - &pcr_data.merchant_account, - payment_intent, - execute_task_process, - &pcr_data.profile, - ) - .await?; + Box::pin(action.execute_payment_task_response_handler( + state, + payment_intent, + execute_task_process, + pcr_data, + &mut pcr_metadata, + )) + .await?; } pcr_types::Decision::Psync(attempt_status, attempt_id) => { diff --git a/crates/router/src/core/passive_churn_recovery/types.rs b/crates/router/src/core/passive_churn_recovery/types.rs index 2bcb7a728b..0f895b6f2e 100644 --- a/crates/router/src/core/passive_churn_recovery/types.rs +++ b/crates/router/src/core/passive_churn_recovery/types.rs @@ -1,10 +1,18 @@ -use common_enums::{self, AttemptStatus, IntentStatus}; +use api_models::{ + enums as api_enums, + mandates::RecurringDetails, + payments::{ + AmountDetails, FeatureMetadata, PaymentRevenueRecoveryMetadata, + PaymentsUpdateIntentRequest, ProxyPaymentsRequest, + }, +}; use common_utils::{self, ext_traits::OptionExt, id_type}; -use diesel_models::{enums, process_tracker::business_status}; +use diesel_models::{enums, process_tracker::business_status, types as diesel_types}; use error_stack::{self, ResultExt}; use hyperswitch_domain_models::{ business_profile, merchant_account, - payments::{PaymentConfirmData, PaymentIntent}, + payments::{PaymentConfirmData, PaymentIntent, PaymentIntentData}, + ApiModelToDieselModelConvertor, }; use time::PrimitiveDateTime; @@ -12,6 +20,7 @@ use crate::{ core::{ errors::{self, RouterResult}, passive_churn_recovery::{self as core_pcr}, + payments::{self, operations::Operation}, }, db::StorageInterface, logger, @@ -68,22 +77,30 @@ impl PcrAttemptStatus { #[derive(Debug, Clone)] pub enum Decision { Execute, - Psync(AttemptStatus, id_type::GlobalAttemptId), + Psync(enums::AttemptStatus, id_type::GlobalAttemptId), InvalidDecision, } impl Decision { pub async fn get_decision_based_on_params( state: &SessionState, - intent_status: IntentStatus, - called_connector: bool, + intent_status: enums::IntentStatus, + called_connector: enums::PaymentConnectorTransmission, active_attempt_id: Option, pcr_data: &storage::passive_churn_recovery::PcrPaymentData, payment_id: &id_type::GlobalPaymentId, ) -> RecoveryResult { Ok(match (intent_status, called_connector, active_attempt_id) { - (IntentStatus::Failed, false, None) => Self::Execute, - (IntentStatus::Processing, true, Some(_)) => { + ( + enums::IntentStatus::Failed, + enums::PaymentConnectorTransmission::ConnectorCallUnsuccessful, + None, + ) => Self::Execute, + ( + enums::IntentStatus::Processing, + enums::PaymentConnectorTransmission::ConnectorCallSucceeded, + Some(_), + ) => { let psync_data = core_pcr::call_psync_api(state, payment_id, pcr_data) .await .change_context(errors::RecoveryError::PaymentCallFailed) @@ -111,13 +128,16 @@ pub enum Action { } impl Action { pub async fn execute_payment( - db: &dyn StorageInterface, + state: &SessionState, merchant_id: &id_type::MerchantId, payment_intent: &PaymentIntent, process: &storage::ProcessTracker, + pcr_data: &storage::passive_churn_recovery::PcrPaymentData, + revenue_recovery_metadata: &PaymentRevenueRecoveryMetadata, ) -> RecoveryResult { - // call the proxy api - let response = call_proxy_api::(payment_intent); + let db = &*state.store; + let response = + call_proxy_api(state, payment_intent, pcr_data, revenue_recovery_metadata).await; // handle proxy api's response match response { Ok(payment_data) => match payment_data.payment_attempt.status.foreign_into() { @@ -148,19 +168,20 @@ impl Action { pub async fn execute_payment_task_response_handler( &self, - db: &dyn StorageInterface, - merchant_account: &merchant_account::MerchantAccount, + state: &SessionState, payment_intent: &PaymentIntent, execute_task_process: &storage::ProcessTracker, - profile: &business_profile::Profile, + pcr_data: &storage::passive_churn_recovery::PcrPaymentData, + revenue_recovery_metadata: &mut PaymentRevenueRecoveryMetadata, ) -> Result<(), errors::ProcessTrackerError> { + let db = &*state.store; match self { Self::SyncPayment(attempt_id) => { core_pcr::insert_psync_pcr_task( db, - merchant_account.get_id().to_owned(), + pcr_data.merchant_account.get_id().to_owned(), payment_intent.id.clone(), - profile.get_id().to_owned(), + pcr_data.profile.get_id().to_owned(), attempt_id.clone(), storage::ProcessTrackerRunner::PassiveRecoveryWorkflow, ) @@ -180,26 +201,61 @@ impl Action { } Self::RetryPayment(schedule_time) => { - let mut pt = execute_task_process.clone(); - // update the schedule time - pt.schedule_time = Some(*schedule_time); - - let pt_task_update = diesel_models::ProcessTrackerUpdate::StatusUpdate { - status: storage::enums::ProcessTrackerStatus::Pending, - business_status: Some(business_status::PENDING.to_owned()), - }; db.as_scheduler() - .update_process(pt.clone(), pt_task_update) + .retry_process(execute_task_process.clone(), *schedule_time) .await?; - // TODO: update the connector called field and make the active attempt None + + // update the connector payment transmission field to Unsuccessful and unset active attempt id + revenue_recovery_metadata.set_payment_transmission_field_for_api_request( + enums::PaymentConnectorTransmission::ConnectorCallUnsuccessful, + ); + + let payment_update_req = PaymentsUpdateIntentRequest::update_feature_metadata_and_active_attempt_with_api( + payment_intent.feature_metadata.clone().unwrap_or_default().convert_back().set_payment_revenue_recovery_metadata_using_api( + revenue_recovery_metadata.clone() + ), + api_enums::UpdateActiveAttempt::Unset, + ); + logger::info!( + "Call made to payments update intent api , with the request body {:?}", + payment_update_req + ); + update_payment_intent_api( + state, + payment_intent.id.clone(), + pcr_data, + payment_update_req, + ) + .await + .change_context(errors::RecoveryError::PaymentCallFailed)?; Ok(()) } Self::TerminalFailure => { // TODO: Record a failure transaction back to Billing Connector + db.as_scheduler() + .finish_process_with_business_status( + execute_task_process.clone(), + business_status::EXECUTE_WORKFLOW_COMPLETE, + ) + .await + .change_context(errors::RecoveryError::ProcessTrackerFailure) + .attach_printable("Failed to update the process tracker")?; Ok(()) } - Self::SuccessfulPayment => Ok(()), + Self::SuccessfulPayment => { + // TODO: Record a successful transaction back to Billing Connector + db.as_scheduler() + .finish_process_with_business_status( + execute_task_process.clone(), + business_status::EXECUTE_WORKFLOW_COMPLETE, + ) + .await + .change_context(errors::RecoveryError::ProcessTrackerFailure) + .attach_printable("Failed to update the process tracker")?; + Ok(()) + } + Self::ReviewPayment => Ok(()), Self::ManualReviewAction => { logger::debug!("Invalid Payment Status For PCR Payment"); @@ -231,30 +287,90 @@ impl Action { } } -// This function would be converted to proxy_payments_core -fn call_proxy_api(payment_intent: &PaymentIntent) -> RouterResult> -where - F: Send + Clone + Sync, -{ - let payment_address = hyperswitch_domain_models::payment_address::PaymentAddress::new( - payment_intent - .shipping_address - .clone() - .map(|address| address.into_inner()), - payment_intent - .billing_address - .clone() - .map(|address| address.into_inner()), - None, - Some(true), - ); - let response = PaymentConfirmData { - flow: std::marker::PhantomData, - payment_intent: payment_intent.clone(), - payment_attempt: todo!(), - payment_method_data: None, - payment_address, - mandate_data: None, +async fn call_proxy_api( + state: &SessionState, + payment_intent: &PaymentIntent, + pcr_data: &storage::passive_churn_recovery::PcrPaymentData, + revenue_recovery: &PaymentRevenueRecoveryMetadata, +) -> RouterResult> { + let operation = payments::operations::proxy_payments_intent::PaymentProxyIntent; + let req = ProxyPaymentsRequest { + return_url: None, + amount: AmountDetails::new(payment_intent.amount_details.clone().into()), + recurring_details: revenue_recovery.get_payment_token_for_api_request(), + shipping: None, + browser_info: None, + connector: revenue_recovery.connector.to_string(), + merchant_connector_id: revenue_recovery.get_merchant_connector_id_for_api_request(), }; - Ok(response) + logger::info!( + "Call made to payments proxy api , with the request body {:?}", + req + ); + + // TODO : Use api handler instead of calling get_tracker and payments_operation_core + // Get the tracker related information. This includes payment intent and payment attempt + let get_tracker_response = operation + .to_get_tracker()? + .get_trackers( + state, + payment_intent.get_id(), + &req, + &pcr_data.merchant_account, + &pcr_data.profile, + &pcr_data.key_store, + &hyperswitch_domain_models::payments::HeaderPayload::default(), + None, + ) + .await?; + + let (payment_data, _req, _, _) = Box::pin(payments::proxy_for_payments_operation_core::< + api_types::Authorize, + _, + _, + _, + PaymentConfirmData, + >( + state, + state.get_req_state(), + pcr_data.merchant_account.clone(), + pcr_data.key_store.clone(), + pcr_data.profile.clone(), + operation, + req, + get_tracker_response, + payments::CallConnectorAction::Trigger, + hyperswitch_domain_models::payments::HeaderPayload::default(), + )) + .await?; + Ok(payment_data) +} + +pub async fn update_payment_intent_api( + state: &SessionState, + global_payment_id: id_type::GlobalPaymentId, + pcr_data: &storage::passive_churn_recovery::PcrPaymentData, + update_req: PaymentsUpdateIntentRequest, +) -> RouterResult> { + // TODO : Use api handler instead of calling payments_intent_operation_core + let operation = payments::operations::PaymentUpdateIntent; + let (payment_data, _req, customer) = payments::payments_intent_operation_core::< + api_types::PaymentUpdateIntent, + _, + _, + PaymentIntentData, + >( + state, + state.get_req_state(), + pcr_data.merchant_account.clone(), + pcr_data.profile.clone(), + pcr_data.key_store.clone(), + operation, + update_req, + global_payment_id, + hyperswitch_domain_models::payments::HeaderPayload::default(), + None, + ) + .await?; + Ok(payment_data) } diff --git a/crates/router/src/core/payments/operations/payment_update_intent.rs b/crates/router/src/core/payments/operations/payment_update_intent.rs index aff169ed91..9b89ead615 100644 --- a/crates/router/src/core/payments/operations/payment_update_intent.rs +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -229,11 +229,11 @@ impl GetTracker, PaymentsUpda }; let active_attempt_id = set_active_attempt_id - .and_then(|active_attempt_req| match active_attempt_req { + .map(|active_attempt_req| match active_attempt_req { UpdateActiveAttempt::Set(global_attempt_id) => Some(global_attempt_id), UpdateActiveAttempt::Unset => None, }) - .or(payment_intent.active_attempt_id); + .unwrap_or(payment_intent.active_attempt_id); let payment_intent = hyperswitch_domain_models::payments::PaymentIntent { amount_details: updated_amount_details, diff --git a/crates/router/src/workflows/passive_churn_recovery_workflow.rs b/crates/router/src/workflows/passive_churn_recovery_workflow.rs index d67f91f116..d817f3687c 100644 --- a/crates/router/src/workflows/passive_churn_recovery_workflow.rs +++ b/crates/router/src/workflows/passive_churn_recovery_workflow.rs @@ -76,14 +76,14 @@ impl ProcessTrackerWorkflow for ExecutePcrWorkflow { match process.name.as_deref() { Some("EXECUTE_WORKFLOW") => { - pcr::perform_execute_payment( + Box::pin(pcr::perform_execute_payment( state, &process, &tracking_data, &pcr_data, key_manager_state, &payment_data.payment_intent, - ) + )) .await } Some("PSYNC_WORKFLOW") => todo!(),