mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 01:57:45 +08:00 
			
		
		
		
	feat(payments): add support for manual retries in payments confirm call (#1170)
This commit is contained in:
		 Abhishek Marrivagu
					Abhishek Marrivagu
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							5e51b6b16d
						
					
				
				
					commit
					1f52a66452
				
			| @ -89,6 +89,7 @@ where | ||||
|             &merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|     authenticate_client_secret( | ||||
|         req.get_client_secret(), | ||||
|         &payment_data.payment_intent, | ||||
|  | ||||
| @ -25,6 +25,7 @@ use crate::{ | ||||
|     core::{ | ||||
|         errors::{self, CustomResult, RouterResult, StorageErrorExt}, | ||||
|         payment_methods::{cards, vault}, | ||||
|         payments, | ||||
|     }, | ||||
|     db::StorageInterface, | ||||
|     routes::{metrics, AppState}, | ||||
| @ -1683,3 +1684,215 @@ pub fn router_data_type_conversion<F1, F2, Req1, Req2, Res1, Res2>( | ||||
|         connector_customer: router_data.connector_customer, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn get_attempt_type( | ||||
|     payment_intent: &storage::PaymentIntent, | ||||
|     payment_attempt: &storage::PaymentAttempt, | ||||
|     request: &api::PaymentsRequest, | ||||
|     action: &str, | ||||
| ) -> RouterResult<AttemptType> { | ||||
|     match payment_intent.status { | ||||
|         enums::IntentStatus::Failed => { | ||||
|             if request.manual_retry { | ||||
|                 match payment_attempt.status { | ||||
|                     enums::AttemptStatus::Started | ||||
|                     | enums::AttemptStatus::AuthenticationPending | ||||
|                     | enums::AttemptStatus::AuthenticationSuccessful | ||||
|                     | enums::AttemptStatus::Authorized | ||||
|                     | enums::AttemptStatus::Charged | ||||
|                     | enums::AttemptStatus::Authorizing | ||||
|                     | enums::AttemptStatus::CodInitiated | ||||
|                     | enums::AttemptStatus::VoidInitiated | ||||
|                     | enums::AttemptStatus::CaptureInitiated | ||||
|                     | enums::AttemptStatus::Unresolved | ||||
|                     | enums::AttemptStatus::Pending | ||||
|                     | enums::AttemptStatus::ConfirmationAwaited | ||||
|                     | enums::AttemptStatus::PartialCharged | ||||
|                     | enums::AttemptStatus::Voided | ||||
|                     | enums::AttemptStatus::AutoRefunded | ||||
|                     | enums::AttemptStatus::PaymentMethodAwaited | ||||
|                     | enums::AttemptStatus::DeviceDataCollectionPending => { | ||||
|                         Err(errors::ApiErrorResponse::InternalServerError) | ||||
|                             .into_report() | ||||
|                             .attach_printable("Payment Attempt unexpected state") | ||||
|                     } | ||||
|  | ||||
|                     storage_enums::AttemptStatus::VoidFailed | ||||
|                     | storage_enums::AttemptStatus::RouterDeclined | ||||
|                     | storage_enums::AttemptStatus::CaptureFailed =>  Err(report!(errors::ApiErrorResponse::PreconditionFailed { | ||||
|                         message: | ||||
|                             format!("You cannot {action} this payment because it has status {}, and the previous attempt has the status {}", payment_intent.status, payment_attempt.status) | ||||
|                         } | ||||
|                     )), | ||||
|  | ||||
|                     storage_enums::AttemptStatus::AuthenticationFailed | ||||
|                     | storage_enums::AttemptStatus::AuthorizationFailed | ||||
|                     | storage_enums::AttemptStatus::Failure => Ok(AttemptType::New), | ||||
|                 } | ||||
|             } else { | ||||
|                 Err(report!(errors::ApiErrorResponse::PreconditionFailed { | ||||
|                         message: | ||||
|                             format!("You cannot {action} this payment because it has status {}, you can pass manual_retry as true in request to try this payment again", payment_intent.status) | ||||
|                         } | ||||
|                     )) | ||||
|             } | ||||
|         } | ||||
|         enums::IntentStatus::Cancelled | ||||
|         | enums::IntentStatus::RequiresCapture | ||||
|         | enums::IntentStatus::Processing | ||||
|         | enums::IntentStatus::Succeeded => { | ||||
|             Err(report!(errors::ApiErrorResponse::PreconditionFailed { | ||||
|                 message: format!( | ||||
|                     "You cannot {action} this payment because it has status {}", | ||||
|                     payment_intent.status, | ||||
|                 ), | ||||
|             })) | ||||
|         } | ||||
|  | ||||
|         enums::IntentStatus::RequiresCustomerAction | ||||
|         | enums::IntentStatus::RequiresMerchantAction | ||||
|         | enums::IntentStatus::RequiresPaymentMethod | ||||
|         | enums::IntentStatus::RequiresConfirmation => Ok(AttemptType::SameOld), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Eq, PartialEq, Clone)] | ||||
| pub enum AttemptType { | ||||
|     New, | ||||
|     SameOld, | ||||
| } | ||||
|  | ||||
| impl AttemptType { | ||||
|     // The function creates a new payment_attempt from the previous payment attempt but doesn't populate fields like payment_method, error_code etc. | ||||
|     // Logic to override the fields with data provided in the request should be done after this if required. | ||||
|     // In case if fields are not overridden by the request then they contain the same data that was in the previous attempt provided it is populated in this function. | ||||
|     #[inline(always)] | ||||
|     fn make_new_payment_attempt( | ||||
|         payment_method_data: &Option<api_models::payments::PaymentMethodData>, | ||||
|         old_payment_attempt: storage::PaymentAttempt, | ||||
|     ) -> storage::PaymentAttemptNew { | ||||
|         let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); | ||||
|  | ||||
|         storage::PaymentAttemptNew { | ||||
|             payment_id: old_payment_attempt.payment_id, | ||||
|             merchant_id: old_payment_attempt.merchant_id, | ||||
|             attempt_id: uuid::Uuid::new_v4().simple().to_string(), | ||||
|  | ||||
|             // A new payment attempt is getting created so, used the same function which is used to populate status in PaymentCreate Flow. | ||||
|             status: payment_attempt_status_fsm(payment_method_data, Some(true)), | ||||
|  | ||||
|             amount: old_payment_attempt.amount, | ||||
|             currency: old_payment_attempt.currency, | ||||
|             save_to_locker: old_payment_attempt.save_to_locker, | ||||
|  | ||||
|             connector: None, | ||||
|  | ||||
|             error_message: None, | ||||
|             offer_amount: old_payment_attempt.offer_amount, | ||||
|             surcharge_amount: old_payment_attempt.surcharge_amount, | ||||
|             tax_amount: old_payment_attempt.tax_amount, | ||||
|             payment_method_id: None, | ||||
|             payment_method: None, | ||||
|             capture_method: old_payment_attempt.capture_method, | ||||
|             capture_on: old_payment_attempt.capture_on, | ||||
|             confirm: old_payment_attempt.confirm, | ||||
|             authentication_type: old_payment_attempt.authentication_type, | ||||
|             created_at, | ||||
|             modified_at, | ||||
|             last_synced, | ||||
|             cancellation_reason: None, | ||||
|             amount_to_capture: old_payment_attempt.amount_to_capture, | ||||
|  | ||||
|             // Once the payment_attempt is authorised then mandate_id is created. If this payment attempt is authorised then mandate_id will be overridden. | ||||
|             // Since mandate_id is a contract between merchant and customer to debit customers amount adding it to newly created attempt | ||||
|             mandate_id: old_payment_attempt.mandate_id, | ||||
|  | ||||
|             // The payment could be done from a different browser or same browser, it would probably be overridden by request data. | ||||
|             browser_info: None, | ||||
|  | ||||
|             error_code: None, | ||||
|             payment_token: None, | ||||
|             connector_metadata: None, | ||||
|             payment_experience: None, | ||||
|             payment_method_type: None, | ||||
|             payment_method_data: None, | ||||
|  | ||||
|             // In case it is passed in create and not in confirm, | ||||
|             business_sub_label: old_payment_attempt.business_sub_label, | ||||
|             // If the algorithm is entered in Create call from server side, it needs to be populated here, however it could be overridden from the request. | ||||
|             straight_through_algorithm: old_payment_attempt.straight_through_algorithm, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn modify_payment_intent_and_payment_attempt( | ||||
|         &self, | ||||
|         request: &api::PaymentsRequest, | ||||
|         fetched_payment_intent: storage::PaymentIntent, | ||||
|         fetched_payment_attempt: storage::PaymentAttempt, | ||||
|         db: &dyn StorageInterface, | ||||
|         storage_scheme: storage::enums::MerchantStorageScheme, | ||||
|     ) -> RouterResult<(storage::PaymentIntent, storage::PaymentAttempt)> { | ||||
|         match self { | ||||
|             Self::SameOld => Ok((fetched_payment_intent, fetched_payment_attempt)), | ||||
|             Self::New => { | ||||
|                 let new_payment_attempt = db | ||||
|                     .insert_payment_attempt( | ||||
|                         Self::make_new_payment_attempt( | ||||
|                             &request.payment_method_data, | ||||
|                             fetched_payment_attempt, | ||||
|                         ), | ||||
|                         storage_scheme, | ||||
|                     ) | ||||
|                     .await | ||||
|                     .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { | ||||
|                         payment_id: fetched_payment_intent.payment_id.to_owned(), | ||||
|                     })?; | ||||
|  | ||||
|                 let updated_payment_intent = db | ||||
|                     .update_payment_intent( | ||||
|                         fetched_payment_intent, | ||||
|                         storage::PaymentIntentUpdate::StatusAndAttemptUpdate { | ||||
|                             status: payment_intent_status_fsm( | ||||
|                                 &request.payment_method_data, | ||||
|                                 Some(true), | ||||
|                             ), | ||||
|                             active_attempt_id: new_payment_attempt.attempt_id.to_owned(), | ||||
|                         }, | ||||
|                         storage_scheme, | ||||
|                     ) | ||||
|                     .await | ||||
|                     .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|  | ||||
|                 Ok((updated_payment_intent, new_payment_attempt)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn get_connector_response( | ||||
|         &self, | ||||
|         payment_attempt: &storage::PaymentAttempt, | ||||
|         db: &dyn StorageInterface, | ||||
|         storage_scheme: storage::enums::MerchantStorageScheme, | ||||
|     ) -> RouterResult<storage::ConnectorResponse> { | ||||
|         match self { | ||||
|             Self::New => db | ||||
|                 .insert_connector_response( | ||||
|                     payments::PaymentCreate::make_connector_response(payment_attempt), | ||||
|                     storage_scheme, | ||||
|                 ) | ||||
|                 .await | ||||
|                 .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { | ||||
|                     payment_id: payment_attempt.payment_id.clone(), | ||||
|                 }), | ||||
|             Self::SameOld => db | ||||
|                 .find_connector_response_by_payment_id_merchant_id_attempt_id( | ||||
|                     &payment_attempt.payment_id, | ||||
|                     &payment_attempt.merchant_id, | ||||
|                     &payment_attempt.attempt_id, | ||||
|                     storage_scheme, | ||||
|                 ) | ||||
|                 .await | ||||
|                 .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -57,20 +57,46 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa | ||||
|             .await | ||||
|             .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|  | ||||
|         helpers::validate_payment_status_against_not_allowed_statuses( | ||||
|             &payment_intent.status, | ||||
|             &[ | ||||
|                 storage_enums::IntentStatus::Cancelled, | ||||
|                 storage_enums::IntentStatus::Succeeded, | ||||
|                 storage_enums::IntentStatus::Processing, | ||||
|                 storage_enums::IntentStatus::RequiresCapture, | ||||
|                 storage_enums::IntentStatus::RequiresMerchantAction, | ||||
|             ], | ||||
|             "confirm", | ||||
|         )?; | ||||
|  | ||||
|         payment_attempt = db | ||||
|             .find_payment_attempt_by_payment_id_merchant_id_attempt_id( | ||||
|                 payment_intent.payment_id.as_str(), | ||||
|                 merchant_id, | ||||
|                 payment_intent.active_attempt_id.as_str(), | ||||
|                 storage_scheme, | ||||
|             ) | ||||
|             .await | ||||
|             .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|  | ||||
|         let attempt_type = | ||||
|             helpers::get_attempt_type(&payment_intent, &payment_attempt, request, "confirm")?; | ||||
|  | ||||
|         (payment_intent, payment_attempt) = attempt_type | ||||
|             .modify_payment_intent_and_payment_attempt( | ||||
|                 request, | ||||
|                 payment_intent, | ||||
|                 payment_attempt, | ||||
|                 db, | ||||
|                 storage_scheme, | ||||
|             ) | ||||
|             .await?; | ||||
|  | ||||
|         payment_intent.setup_future_usage = request | ||||
|             .setup_future_usage | ||||
|             .map(ForeignInto::foreign_into) | ||||
|             .or(payment_intent.setup_future_usage); | ||||
|  | ||||
|         helpers::validate_payment_status_against_not_allowed_statuses( | ||||
|             &payment_intent.status, | ||||
|             &[ | ||||
|                 storage_enums::IntentStatus::Failed, | ||||
|                 storage_enums::IntentStatus::Succeeded, | ||||
|             ], | ||||
|             "confirm", | ||||
|         )?; | ||||
|  | ||||
|         let (token, payment_method, setup_mandate) = helpers::get_token_pm_type_mandate_details( | ||||
|             state, | ||||
|             request, | ||||
| @ -88,16 +114,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa | ||||
|                 field_name: "browser_info", | ||||
|             })?; | ||||
|  | ||||
|         payment_attempt = db | ||||
|             .find_payment_attempt_by_payment_id_merchant_id_attempt_id( | ||||
|                 payment_intent.payment_id.as_str(), | ||||
|                 merchant_id, | ||||
|                 payment_intent.active_attempt_id.as_str(), | ||||
|                 storage_scheme, | ||||
|             ) | ||||
|             .await | ||||
|             .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|  | ||||
|         let token = token.or_else(|| payment_attempt.payment_token.clone()); | ||||
|  | ||||
|         helpers::validate_pm_or_token_given( | ||||
| @ -119,6 +135,11 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa | ||||
|             .payment_experience | ||||
|             .map(|experience| experience.foreign_into()); | ||||
|  | ||||
|         payment_attempt.capture_method = request | ||||
|             .capture_method | ||||
|             .or(payment_attempt.capture_method.map(|cm| cm.foreign_into())) | ||||
|             .map(|cm| cm.foreign_into()); | ||||
|  | ||||
|         currency = payment_attempt.currency.get_required_value("currency")?; | ||||
|         amount = payment_attempt.amount.into(); | ||||
|  | ||||
| @ -149,15 +170,9 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         connector_response = db | ||||
|             .find_connector_response_by_payment_id_merchant_id_attempt_id( | ||||
|                 &payment_attempt.payment_id, | ||||
|                 &payment_attempt.merchant_id, | ||||
|                 &payment_attempt.attempt_id, | ||||
|                 storage_scheme, | ||||
|             ) | ||||
|             .await | ||||
|             .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|         connector_response = attempt_type | ||||
|             .get_connector_response(&payment_attempt, db, storage_scheme) | ||||
|             .await?; | ||||
|  | ||||
|         payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); | ||||
|         payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user