diff --git a/.typos.toml b/.typos.toml index 612aa3be8bc..0316374cd6b 100644 --- a/.typos.toml +++ b/.typos.toml @@ -74,6 +74,7 @@ Corse-du-Sud = "Corse-du-Sud" # Is a state in France Haute-Corse = "Haute-Corse" # Is a state in France Fram = "Fram" # Is a state in Slovenia + [default.extend-words] aci = "aci" # Name of a connector afe = "afe" # Commit id diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index ccda5fd402d..eddc8ce5fce 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -3,7 +3,7 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; #[cfg(feature = "v2")] use super::{ PaymentStartRedirectionRequest, PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, - PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest, + PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest, ProxyPaymentsIntentResponse, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -194,6 +194,15 @@ impl ApiEventMetric for PaymentsConfirmIntentResponse { } } +#[cfg(feature = "v2")] +impl ApiEventMetric for ProxyPaymentsIntentResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} + #[cfg(feature = "v2")] impl ApiEventMetric for super::PaymentsRetrieveResponse { fn get_api_event_type(&self) -> Option { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 22e845c5e0c..5a9bc55a232 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -34,6 +34,8 @@ use utoipa::ToSchema; #[cfg(feature = "v1")] use crate::ephemeral_key::EphemeralKeyCreateResponse; #[cfg(feature = "v2")] +use crate::mandates::ProcessorPaymentToken; +#[cfg(feature = "v2")] use crate::payment_methods; use crate::{ admin::{self, MerchantConnectorInfo}, @@ -5099,6 +5101,32 @@ pub struct PaymentsConfirmIntentRequest { pub browser_info: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct ProxyPaymentsRequest { + /// The URL to which you want the user to be redirected after the completion of the payment operation + /// If this url is not passed, the url configured in the business profile will be used + #[schema(value_type = Option, example = "https://hyperswitch.io")] + pub return_url: Option, + + pub amount: AmountDetails, + + pub recurring_details: ProcessorPaymentToken, + + pub shipping: Option
, + + /// Additional details required by 3DS 2.0 + #[schema(value_type = Option)] + pub browser_info: Option, + + #[schema(example = "stripe")] + pub connector: String, + + #[schema(value_type = String)] + pub merchant_connector_id: id_type::MerchantConnectorAccountId, +} + // This struct contains the union of fields in `PaymentsCreateIntentRequest` and // `PaymentsConfirmIntentRequest` #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ToSchema)] @@ -5488,6 +5516,73 @@ pub struct PaymentsConfirmIntentResponse { pub applied_authentication_type: api_enums::AuthenticationType, } +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct ProxyPaymentsIntentResponse { + /// Unique identifier for the payment. This ensures idempotency for multiple payments + /// that have been done by a single merchant. + #[schema( + min_length = 32, + max_length = 64, + example = "12345_pay_01926c58bc6e77c09e809964e72af8c8", + value_type = String, + )] + pub id: id_type::GlobalPaymentId, + + #[schema(value_type = IntentStatus, example = "success")] + pub status: api_enums::IntentStatus, + + /// Amount related information for this payment and attempt + pub amount: PaymentAmountDetailsResponse, + + /// The connector used for the payment + #[schema(example = "stripe")] + pub connector: String, + + /// It's a token used for client side verification. + #[schema(value_type = String)] + pub client_secret: common_utils::types::ClientSecret, + + /// Time when the payment was created + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created: PrimitiveDateTime, + + /// The payment method information provided for making a payment + #[schema(value_type = Option)] + #[serde(serialize_with = "serialize_payment_method_data_response")] + pub payment_method_data: Option, + + /// The payment method type for this payment attempt + #[schema(value_type = PaymentMethod, example = "wallet")] + pub payment_method_type: api_enums::PaymentMethod, + + #[schema(value_type = PaymentMethodType, example = "apple_pay")] + pub payment_method_subtype: api_enums::PaymentMethodType, + + /// Additional information required for redirection + pub next_action: Option, + + /// A unique identifier for a payment provided by the connector + #[schema(value_type = Option, example = "993672945374576J")] + pub connector_transaction_id: Option, + + /// reference(Identifier) to the payment at connector side + #[schema(value_type = Option, example = "993672945374576J")] + pub connector_reference_id: Option, + + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment + #[schema(value_type = String)] + pub merchant_connector_id: id_type::MerchantConnectorAccountId, + + /// The browser information used for this payment + #[schema(value_type = Option)] + pub browser_info: Option, + + /// Error details for the payment if any + pub error: Option, +} + /// Token information that can be used to initiate transactions by the merchant. #[cfg(feature = "v2")] #[derive(Debug, Serialize, ToSchema)] diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 9409be94163..7f81a57e639 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -318,6 +318,7 @@ pub struct PaymentAttemptNew { pub capture_before: Option, pub charges: Option, pub feature_metadata: Option, + pub connector: Option, } #[cfg(feature = "v1")] @@ -3545,7 +3546,6 @@ pub struct PaymentAttemptFeatureMetadata { pub struct PaymentAttemptRecoveryData { pub attempt_triggered_by: common_enums::TriggeredBy, } - #[cfg(feature = "v2")] common_utils::impl_to_sql_from_sql_json!(PaymentAttemptFeatureMetadata); diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index f10616ceb1b..eb05d352154 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -225,11 +225,14 @@ impl TaxDetails { /// Get the tax amount /// If default tax is present, return the default tax amount /// If default tax is not present, return the tax amount based on the payment method if it matches the provided payment method type - pub fn get_tax_amount(&self, payment_method: PaymentMethodType) -> Option { + pub fn get_tax_amount(&self, payment_method: Option) -> Option { self.payment_method_type .as_ref() - .filter(|payment_method_type_tax| payment_method_type_tax.pmt == payment_method) - .map(|payment_method_type_tax| payment_method_type_tax.order_tax_amount) + .zip(payment_method) + .filter(|(payment_method_type_tax, payment_method)| { + payment_method_type_tax.pmt == *payment_method + }) + .map(|(payment_method_type_tax, _)| payment_method_type_tax.order_tax_amount) .or_else(|| self.get_default_tax_amount()) } diff --git a/crates/diesel_models/src/types.rs b/crates/diesel_models/src/types.rs index 6bc8aee2893..04bd0e925cc 100644 --- a/crates/diesel_models/src/types.rs +++ b/crates/diesel_models/src/types.rs @@ -1,5 +1,5 @@ #[cfg(feature = "v2")] -use common_enums::{enums::PaymentConnectorTransmission, PaymentMethod, PaymentMethodType}; +use common_enums::enums::PaymentConnectorTransmission; use common_utils::{hashing::HashedString, pii, types::MinorUnit}; use diesel::{ sql_types::{Json, Jsonb}, @@ -137,9 +137,9 @@ pub struct PaymentRevenueRecoveryMetadata { /// Billing Connector Payment Details pub billing_connector_payment_details: BillingConnectorPaymentDetails, ///Payment Method Type - pub payment_method_type: PaymentMethod, + pub payment_method_type: common_enums::enums::PaymentMethod, /// PaymentMethod Subtype - pub payment_method_subtype: PaymentMethodType, + pub payment_method_subtype: common_enums::enums::PaymentMethodType, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 606a8af51c3..c74cb38402b 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -225,7 +225,7 @@ impl AmountDetails { let order_tax_amount = match self.skip_external_tax_calculation { common_enums::TaxCalculationOverride::Skip => { self.tax_details.as_ref().and_then(|tax_details| { - tax_details.get_tax_amount(confirm_intent_request.payment_method_subtype) + tax_details.get_tax_amount(Some(confirm_intent_request.payment_method_subtype)) }) } common_enums::TaxCalculationOverride::Calculate => None, @@ -243,6 +243,42 @@ impl AmountDetails { }) } + pub fn proxy_create_attempt_amount_details( + &self, + _confirm_intent_request: &api_models::payments::ProxyPaymentsRequest, + ) -> payment_attempt::AttemptAmountDetails { + let net_amount = self.calculate_net_amount(); + + let surcharge_amount = match self.skip_surcharge_calculation { + common_enums::SurchargeCalculationOverride::Skip => self.surcharge_amount, + common_enums::SurchargeCalculationOverride::Calculate => None, + }; + + let tax_on_surcharge = match self.skip_surcharge_calculation { + common_enums::SurchargeCalculationOverride::Skip => self.tax_on_surcharge, + common_enums::SurchargeCalculationOverride::Calculate => None, + }; + + let order_tax_amount = match self.skip_external_tax_calculation { + common_enums::TaxCalculationOverride::Skip => self + .tax_details + .as_ref() + .and_then(|tax_details| tax_details.get_tax_amount(None)), + common_enums::TaxCalculationOverride::Calculate => None, + }; + + payment_attempt::AttemptAmountDetails::from(payment_attempt::AttemptAmountDetailsSetter { + net_amount, + amount_to_capture: None, + surcharge_amount, + tax_on_surcharge, + // This will be updated when we receive response from the connector + amount_capturable: MinorUnit::zero(), + shipping_cost: self.shipping_cost, + order_tax_amount, + }) + } + pub fn update_from_request(self, req: &api_models::payments::AmountDetailsUpdate) -> Self { Self { order_amount: req @@ -585,7 +621,7 @@ where // TODO: Check if this can be merged with existing payment data #[cfg(feature = "v2")] -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PaymentConfirmData where F: Clone, @@ -595,6 +631,7 @@ where pub payment_attempt: PaymentAttempt, pub payment_method_data: Option, pub payment_address: payment_address::PaymentAddress, + pub mandate_data: Option, } #[cfg(feature = "v2")] diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index f512711987b..8bfafc58c53 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -513,6 +513,14 @@ impl PaymentAttempt { .transpose() .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) .attach_printable("Unable to decode billing address")?; + + let connector_token = Some(diesel_models::ConnectorTokenDetails { + connector_mandate_id: None, + connector_mandate_request_reference_id: Some(common_utils::generate_id_with_len( + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, + )), + }); + let authentication_type = payment_intent.authentication_type.unwrap_or_default(); Ok(Self { @@ -559,12 +567,102 @@ impl PaymentAttempt { external_reference_id: None, payment_method_billing_address, error: None, - connector_token_details: Some(diesel_models::ConnectorTokenDetails { - connector_mandate_id: None, - connector_mandate_request_reference_id: Some(common_utils::generate_id_with_len( - consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, - )), - }), + connector_token_details: connector_token, + id, + card_discovery: None, + feature_metadata: None, + }) + } + + #[cfg(feature = "v2")] + pub async fn proxy_create_domain_model( + payment_intent: &super::PaymentIntent, + cell_id: id_type::CellId, + storage_scheme: storage_enums::MerchantStorageScheme, + request: &api_models::payments::ProxyPaymentsRequest, + encrypted_data: DecryptedPaymentAttempt, + ) -> CustomResult { + let id = id_type::GlobalAttemptId::generate(&cell_id); + let intent_amount_details = payment_intent.amount_details.clone(); + + let attempt_amount_details = + intent_amount_details.proxy_create_attempt_amount_details(request); + + let now = common_utils::date_time::now(); + let payment_method_billing_address = encrypted_data + .payment_method_billing_address + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode billing address")?; + let connector_token = Some(diesel_models::ConnectorTokenDetails { + connector_mandate_id: None, + connector_mandate_request_reference_id: Some(common_utils::generate_id_with_len( + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, + )), + }); + let payment_method_type_data = payment_intent + .feature_metadata + .as_ref() + .and_then(|fm| fm.payment_revenue_recovery_metadata.as_ref()) + .map(|rrm| rrm.payment_method_type); + + let payment_method_subtype_data = payment_intent + .feature_metadata + .as_ref() + .and_then(|fm| fm.payment_revenue_recovery_metadata.as_ref()) + .map(|rrm| rrm.payment_method_subtype); + + let authentication_type = payment_intent.authentication_type.unwrap_or_default(); + Ok(Self { + payment_id: payment_intent.id.clone(), + merchant_id: payment_intent.merchant_id.clone(), + amount_details: attempt_amount_details, + status: common_enums::AttemptStatus::Started, + connector: Some(request.connector.clone()), + authentication_type, + created_at: now, + modified_at: now, + last_synced: None, + cancellation_reason: None, + browser_info: request.browser_info.clone(), + payment_token: None, + connector_metadata: None, + payment_experience: None, + payment_method_data: None, + routing_result: None, + preprocessing_step_id: None, + multiple_capture_count: None, + connector_response_reference_id: None, + updated_by: storage_scheme.to_string(), + redirection_data: None, + encoded_data: None, + merchant_connector_id: Some(request.merchant_connector_id.clone()), + external_three_ds_authentication_attempted: None, + authentication_connector: None, + authentication_id: None, + fingerprint_id: None, + charges: None, + client_source: None, + client_version: None, + customer_acceptance: None, + profile_id: payment_intent.profile_id.clone(), + organization_id: payment_intent.organization_id.clone(), + payment_method_type: payment_method_type_data + .unwrap_or(common_enums::PaymentMethod::Card), + payment_method_id: None, + connector_payment_id: None, + payment_method_subtype: payment_method_subtype_data + .unwrap_or(common_enums::PaymentMethodType::Credit), + authentication_applied: None, + external_reference_id: None, + payment_method_billing_address, + error: None, + connector_token_details: connector_token, feature_metadata: None, id, card_discovery: None, @@ -2191,6 +2289,7 @@ impl behaviour::Conversion for PaymentAttempt { request_extended_authorization: None, capture_before: None, feature_metadata: feature_metadata.as_ref().map(From::from), + connector, }) } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index cc1a7054ded..caf439bedaa 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -326,6 +326,7 @@ pub enum PaymentIntentUpdate { status: common_enums::IntentStatus, amount_captured: Option, updated_by: String, + feature_metadata: Option>, }, /// SyncUpdate of ConfirmIntent in PostUpdateTrackers SyncUpdate { @@ -436,6 +437,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { status, updated_by, amount_captured, + feature_metadata, } => Self { status: Some(status), active_attempt_id: None, @@ -464,7 +466,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { allowed_payment_method_types: None, metadata: None, connector_metadata: None, - feature_metadata: None, + feature_metadata: feature_metadata.map(|val| *val), payment_link_config: None, request_incremental_authorization: None, session_expiry: None, diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index e3a8264217c..b15206347f2 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -474,6 +474,37 @@ impl storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { let amount_captured = self.get_captured_amount(payment_data); + let status = payment_data.payment_attempt.status.is_terminal_status(); + let mut updated_feature_metadata = payment_data + .payment_intent + .feature_metadata + .as_ref() + .map(|fm| { + let mut updated_metadata = fm.clone(); + if let Some(ref mut rrm) = updated_metadata.payment_revenue_recovery_metadata { + rrm.payment_connector_transmission = + common_enums::PaymentConnectorTransmission::ConnectorCallSucceeded; + } + updated_metadata + }); + if status { + updated_feature_metadata = + payment_data + .payment_intent + .feature_metadata + .as_ref() + .map(|fm| { + let mut updated_metadata = fm.clone(); + if let Some(ref mut rrm) = + updated_metadata.payment_revenue_recovery_metadata + { + rrm.payment_connector_transmission = + common_enums::PaymentConnectorTransmission::ConnectorCallFailed; + } + updated_metadata + }); + } + match self.response { Ok(ref _response) => PaymentIntentUpdate::ConfirmIntentPostUpdate { status: common_enums::IntentStatus::from( @@ -481,6 +512,7 @@ impl ), amount_captured, updated_by: storage_scheme.to_string(), + feature_metadata: updated_feature_metadata.map(Box::new), }, Err(ref error) => PaymentIntentUpdate::ConfirmIntentPostUpdate { status: error @@ -489,6 +521,7 @@ impl .unwrap_or(common_enums::IntentStatus::Failed), amount_captured, updated_by: storage_scheme.to_string(), + feature_metadata: updated_feature_metadata.map(Box::new), }, } } @@ -1127,6 +1160,19 @@ impl storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { let amount_captured = self.get_captured_amount(payment_data); + let updated_feature_metadata = + payment_data + .payment_intent + .feature_metadata + .as_ref() + .map(|fm| { + let mut updated_metadata = fm.clone(); + if let Some(ref mut rrm) = updated_metadata.payment_revenue_recovery_metadata { + rrm.payment_connector_transmission = + common_enums::PaymentConnectorTransmission::ConnectorCallSucceeded; + } + updated_metadata + }); match self.response { Ok(ref _response) => PaymentIntentUpdate::ConfirmIntentPostUpdate { status: common_enums::IntentStatus::from( @@ -1134,6 +1180,7 @@ impl ), amount_captured, updated_by: storage_scheme.to_string(), + feature_metadata: updated_feature_metadata.map(Box::new), }, Err(ref error) => PaymentIntentUpdate::ConfirmIntentPostUpdate { status: error @@ -1142,6 +1189,7 @@ impl .unwrap_or(common_enums::IntentStatus::Failed), amount_captured, updated_by: storage_scheme.to_string(), + feature_metadata: updated_feature_metadata.map(Box::new), }, } } diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 071a77aa7bd..d4dcd337f82 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -72,7 +72,6 @@ pub struct PaymentsAuthorizeData { pub shipping_cost: Option, pub additional_payment_method_data: Option, } - #[derive(Debug, Clone)] pub struct PaymentsPostSessionTokensData { // amount here would include amount, surcharge_amount and shipping_cost diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 17775b83aab..7d013efaa10 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1077,6 +1077,95 @@ where )) } +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +#[instrument(skip_all, fields(payment_id, merchant_id))] +pub async fn proxy_for_payments_operation_core( + state: &SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + profile: domain::Profile, + operation: Op, + req: Req, + get_tracker_response: operations::GetTrackerResponse, + call_connector_action: CallConnectorAction, + header_payload: HeaderPayload, +) -> RouterResult<(D, Req, Option, Option)> +where + F: Send + Clone + Sync, + Req: Send + Sync, + Op: Operation + Send + Sync, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + + // To create connector flow specific interface data + D: ConstructFlowSpecificData, + RouterData: Feature, + + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, + + RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, + + // To perform router related operation for PaymentResponse + PaymentResponse: Operation, + FData: Send + Sync + Clone, +{ + let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation); + + // Get the trackers related to track the state of the payment + let operations::GetTrackerResponse { mut payment_data } = get_tracker_response; + // consume the req merchant_connector_id and set it in the payment_data + let connector = operation + .to_domain()? + .perform_routing( + &merchant_account, + &profile, + state, + &mut payment_data, + &key_store, + ) + .await?; + + let payment_data = match connector { + ConnectorCallType::PreDetermined(connector_data) => { + let router_data = proxy_for_call_connector_service( + state, + req_state.clone(), + &merchant_account, + &key_store, + connector_data.clone(), + &operation, + &mut payment_data, + call_connector_action.clone(), + header_payload.clone(), + &profile, + ) + .await?; + + let payments_response_operation = Box::new(PaymentResponse); + + payments_response_operation + .to_post_update_tracker()? + .update_tracker( + state, + payment_data, + router_data, + &key_store, + merchant_account.storage_scheme, + ) + .await? + } + ConnectorCallType::Retryable(vec) => todo!(), + ConnectorCallType::SessionMultiple(vec) => todo!(), + ConnectorCallType::Skip => payment_data, + }; + + Ok((payment_data, req, None, None)) +} + #[cfg(feature = "v2")] #[allow(clippy::too_many_arguments, clippy::type_complexity)] #[instrument(skip_all, fields(payment_id, merchant_id))] @@ -1541,6 +1630,85 @@ where ) } +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn proxy_for_payments_core( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + operation: Op, + req: Req, + payment_id: id_type::GlobalPaymentId, + call_connector_action: CallConnectorAction, + header_payload: HeaderPayload, +) -> RouterResponse +where + F: Send + Clone + Sync, + Req: Send + Sync, + FData: Send + Sync + Clone, + Op: Operation + ValidateStatusForOperation + Send + Sync + Clone, + Req: Debug, + D: OperationSessionGetters + + OperationSessionSetters + + transformers::GenerateResponse + + Send + + Sync + + Clone, + D: ConstructFlowSpecificData, + RouterData: Feature, + + dyn api::Connector: + services::api::ConnectorIntegration, + + PaymentResponse: Operation, + + RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, +{ + operation + .to_validate_request()? + .validate_request(&req, &merchant_account)?; + + let get_tracker_response = operation + .to_get_tracker()? + .get_trackers( + &state, + &payment_id, + &req, + &merchant_account, + &profile, + &key_store, + &header_payload, + None, + ) + .await?; + + let (payment_data, _req, connector_http_status_code, external_latency) = + proxy_for_payments_operation_core::<_, _, _, _, _>( + &state, + req_state, + merchant_account.clone(), + key_store, + profile, + operation.clone(), + req, + get_tracker_response, + call_connector_action, + header_payload.clone(), + ) + .await?; + + payment_data.generate_response( + &state, + connector_http_status_code, + external_latency, + header_payload.x_hs_latency, + &merchant_account, + ) +} + #[cfg(feature = "v2")] #[allow(clippy::too_many_arguments)] pub async fn payments_intent_core( @@ -3331,6 +3499,135 @@ where Ok((router_data, merchant_connector_account)) } +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn proxy_for_call_connector_service( + state: &SessionState, + req_state: ReqState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + connector: api::ConnectorData, + operation: &BoxedOperation<'_, F, ApiRequest, D>, + payment_data: &mut D, + call_connector_action: CallConnectorAction, + header_payload: HeaderPayload, + business_profile: &domain::Profile, +) -> RouterResult> +where + F: Send + Clone + Sync, + RouterDReq: Send + Sync, + + // To create connector flow specific interface data + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + D: ConstructFlowSpecificData, + RouterData: Feature + Send, + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, +{ + let stime_connector = Instant::now(); + + let merchant_connector_id = connector + .merchant_connector_id + .as_ref() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("connector id is not set")?; + + let merchant_connector_account = state + .store + .find_merchant_connector_account_by_id(&state.into(), merchant_connector_id, key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_connector_id.get_string_repr().to_owned(), + })?; + + let mut router_data = payment_data + .construct_router_data( + state, + connector.connector.id(), + merchant_account, + key_store, + &None, + &merchant_connector_account, + None, + None, + ) + .await?; + + let add_access_token_result = router_data + .add_access_token( + state, + &connector, + merchant_account, + payment_data.get_creds_identifier(), + ) + .await?; + + router_data = router_data.add_session_token(state, &connector).await?; + + let mut should_continue_further = access_token::update_router_data_with_access_token_result( + &add_access_token_result, + &mut router_data, + &call_connector_action, + ); + + (router_data, should_continue_further) = complete_preprocessing_steps_if_required( + state, + &connector, + payment_data, + router_data, + operation, + should_continue_further, + ) + .await?; + + let (connector_request, should_continue_further) = if should_continue_further { + router_data + .build_flow_specific_connector_request(state, &connector, call_connector_action.clone()) + .await? + } else { + (None, false) + }; + + (_, *payment_data) = operation + .to_update_tracker()? + .update_trackers( + state, + req_state, + payment_data.clone(), + None, + merchant_account.storage_scheme, + None, + key_store, + None, + header_payload.clone(), + ) + .await?; + + let router_data = if should_continue_further { + router_data + .decide_flows( + state, + &connector, + call_connector_action, + connector_request, + business_profile, + header_payload.clone(), + ) + .await + } else { + Ok(router_data) + }?; + + let etime_connector = Instant::now(); + let duration_connector = etime_connector.saturating_duration_since(stime_connector); + tracing::info!(duration = format!("Duration taken: {}", duration_connector.as_millis())); + + Ok(router_data) +} + pub async fn add_decrypted_payment_method_token( tokenization_action: TokenizationAction, payment_data: &D, @@ -7634,7 +7931,7 @@ pub trait OperationSessionGetters { fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails>; fn get_force_sync(&self) -> Option; fn get_capture_method(&self) -> Option; - + fn get_merchant_connector_id_in_attempt(&self) -> Option; #[cfg(feature = "v2")] fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt>; } @@ -7703,7 +8000,9 @@ impl OperationSessionGetters for PaymentData { fn get_address(&self) -> &PaymentAddress { &self.address } - + fn get_merchant_connector_id_in_attempt(&self) -> Option { + self.payment_attempt.merchant_connector_id.clone() + } fn get_creds_identifier(&self) -> Option<&str> { self.creds_identifier.as_deref() } @@ -8024,6 +8323,10 @@ impl OperationSessionGetters for PaymentIntentData { todo!() } + fn get_merchant_connector_id_in_attempt(&self) -> Option { + todo!() + } + fn get_billing_address(&self) -> Option { todo!() } @@ -8233,7 +8536,11 @@ impl OperationSessionGetters for PaymentConfirmData { } fn get_payment_attempt_connector(&self) -> Option<&str> { - todo!() + self.payment_attempt.connector.as_deref() + } + + fn get_merchant_connector_id_in_attempt(&self) -> Option { + self.payment_attempt.merchant_connector_id.clone() } fn get_billing_address(&self) -> Option { @@ -8450,6 +8757,10 @@ impl OperationSessionGetters for PaymentStatusData { todo!() } + fn get_merchant_connector_id_in_attempt(&self) -> Option { + todo!() + } + fn get_billing_address(&self) -> Option { todo!() } @@ -8664,6 +8975,10 @@ impl OperationSessionGetters for PaymentCaptureData { todo!() } + fn get_merchant_connector_id_in_attempt(&self) -> Option { + todo!() + } + fn get_billing_address(&self) -> Option { todo!() } diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index a1ebaa04152..b5110c5c45a 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -32,6 +32,9 @@ pub mod tax_calculation; #[cfg(feature = "v2")] pub mod payment_confirm_intent; +#[cfg(feature = "v2")] +pub mod proxy_payments_intent; + #[cfg(feature = "v2")] pub mod payment_create_intent; #[cfg(feature = "v2")] diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 79ebc2d8a38..8727ea47756 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -254,6 +254,7 @@ impl GetTracker, PaymentsConfir payment_attempt, payment_method_data, payment_address, + mandate_data: None, }; let get_trackers_response = operations::GetTrackerResponse { payment_data }; diff --git a/crates/router/src/core/payments/operations/proxy_payments_intent.rs b/crates/router/src/core/payments/operations/proxy_payments_intent.rs new file mode 100644 index 00000000000..28cb1e3b3c9 --- /dev/null +++ b/crates/router/src/core/payments/operations/proxy_payments_intent.rs @@ -0,0 +1,473 @@ +use api_models::{ + enums::FrmSuggestion, + payments::{ConnectorMandateReferenceId, MandateIds, MandateReferenceId, ProxyPaymentsRequest}, +}; +// use diesel_models::payment_attempt::ConnectorMandateReferenceId; +use async_trait::async_trait; +use common_enums::enums; +use common_utils::types::keymanager::ToEncryptable; +use error_stack::ResultExt; +use hyperswitch_domain_models::{ + payment_method_data::PaymentMethodData, payments::PaymentConfirmData, +}; +use masking::PeekInterface; +use router_env::{instrument, tracing}; + +use super::{Domain, GetTracker, Operation, PostUpdateTracker, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payments::operations::{self, ValidateStatusForOperation}, + }, + routes::{app::ReqState, SessionState}, + types::{ + self, + api::{self, ConnectorCallType}, + domain::{self, types as domain_types}, + storage::{self, enums as storage_enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentProxyIntent; + +impl ValidateStatusForOperation for PaymentProxyIntent { + /// Validate if the current operation can be performed on the current status of the payment intent + fn validate_status_for_operation( + &self, + intent_status: common_enums::IntentStatus, + ) -> Result<(), errors::ApiErrorResponse> { + match intent_status { + //Failed state is included here so that in PCR, retries can be done for failed payments, otherwise for a failed attempt it was asking for new payment_intent + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::Failed => Ok(()), + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: format!("{self:?}"), + field_name: "status".to_string(), + current_value: intent_status.to_string(), + states: ["requires_payment_method".to_string()].join(", "), + }) + } + } + } +} +type BoxedConfirmOperation<'b, F> = + super::BoxedOperation<'b, F, ProxyPaymentsRequest, PaymentConfirmData>; + +impl Operation for &PaymentProxyIntent { + type Data = PaymentConfirmData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(*self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(*self) + } +} + +#[automatically_derived] +impl Operation for PaymentProxyIntent { + type Data = PaymentConfirmData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&dyn Domain> { + Ok(self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(self) + } +} + +impl ValidateRequest> + for PaymentProxyIntent +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &ProxyPaymentsRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult { + let validate_result = operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }; + + Ok(validate_result) + } +} + +#[async_trait] +impl GetTracker, ProxyPaymentsRequest> + for PaymentProxyIntent +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &common_utils::id_type::GlobalPaymentId, + request: &ProxyPaymentsRequest, + merchant_account: &domain::MerchantAccount, + _profile: &domain::Profile, + key_store: &domain::MerchantKeyStore, + header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, + ) -> RouterResult>> { + let db = &*state.store; + let key_manager_state = &state.into(); + + let storage_scheme = merchant_account.storage_scheme; + + let payment_intent = db + .find_payment_intent_by_id(key_manager_state, payment_id, key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + self.validate_status_for_operation(payment_intent.status)?; + + let client_secret = header_payload + .client_secret + .as_ref() + .get_required_value("client_secret header")?; + payment_intent.validate_client_secret(client_secret)?; + + let cell_id = state.conf.cell_information.id.clone(); + + let batch_encrypted_data = domain_types::crypto_operation( + key_manager_state, + common_utils::type_name!(hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::to_encryptable( + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt { + payment_method_billing_address: None, + }, + ), + ), + common_utils::types::keymanager::Identifier::Merchant(merchant_account.get_id().to_owned()), + key_store.key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details".to_string())?; + + let encrypted_data = + hyperswitch_domain_models::payments::payment_attempt::FromRequestEncryptablePaymentAttempt::from_encryptable(batch_encrypted_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment intent details")?; + + let payment_attempt_domain_model: hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt = + hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt::proxy_create_domain_model( + &payment_intent, + cell_id, + storage_scheme, + request, + encrypted_data + ) + .await?; + + let payment_attempt = db + .insert_payment_attempt( + key_manager_state, + key_store, + payment_attempt_domain_model, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not insert payment attempt")?; + let processor_payment_token = request.recurring_details.processor_payment_token.clone(); + + 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()), + payment_attempt + .payment_method_billing_address + .clone() + .map(|address| address.into_inner()), + Some(true), + ); + let mandate_data_input = MandateIds { + mandate_id: None, + mandate_reference_id: Some(MandateReferenceId::ConnectorMandateId( + ConnectorMandateReferenceId::new( + Some(processor_payment_token), + None, + None, + None, + None, + ), + )), + }; + let payment_data = PaymentConfirmData { + flow: std::marker::PhantomData, + payment_intent, + payment_attempt, + payment_method_data: Some(PaymentMethodData::MandatePayment), + payment_address, + mandate_data: Some(mandate_data_input), + }; + + let get_trackers_response = operations::GetTrackerResponse { payment_data }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl Domain> + for PaymentProxyIntent +{ + async fn get_customer_details<'a>( + &'a self, + _state: &SessionState, + _payment_data: &mut PaymentConfirmData, + _merchant_key_store: &domain::MerchantKeyStore, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult<(BoxedConfirmOperation<'a, F>, Option), errors::StorageError> + { + Ok((Box::new(self), None)) + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a SessionState, + _payment_data: &mut PaymentConfirmData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + BoxedConfirmOperation<'a, F>, + Option, + Option, + )> { + Ok((Box::new(self), None, None)) + } + + async fn perform_routing<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, + state: &SessionState, + payment_data: &mut PaymentConfirmData, + _mechant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + use crate::core::payments::OperationSessionGetters; + + let connector_name = payment_data.get_payment_attempt_connector(); + if let Some(connector_name) = connector_name { + let merchant_connector_id = payment_data.get_merchant_connector_id_in_attempt(); + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_name, + api::GetToken::Connector, + merchant_connector_id, + )?; + + Ok(ConnectorCallType::PreDetermined(connector_data)) + } else { + Err(error_stack::Report::new( + errors::ApiErrorResponse::InternalServerError, + )) + } + } +} +#[async_trait] +impl UpdateTracker, ProxyPaymentsRequest> + for PaymentProxyIntent +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + state: &'b SessionState, + _req_state: ReqState, + mut payment_data: PaymentConfirmData, + _customer: Option, + storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult<(BoxedConfirmOperation<'b, F>, PaymentConfirmData)> + where + F: 'b + Send, + { + let db = &*state.store; + let key_manager_state = &state.into(); + + let intent_status = common_enums::IntentStatus::Processing; + let attempt_status = common_enums::AttemptStatus::Pending; + + let connector = payment_data + .payment_attempt + .connector + .clone() + .get_required_value("connector") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Connector is none when constructing response")?; + + let merchant_connector_id = payment_data + .payment_attempt + .merchant_connector_id + .clone() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant connector id is none when constructing response")?; + + let payment_intent_update = + hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::ConfirmIntent { + status: intent_status, + updated_by: storage_scheme.to_string(), + active_attempt_id: payment_data.payment_attempt.id.clone(), + }; + + let authentication_type = payment_data + .payment_intent + .authentication_type + .unwrap_or_default(); + + let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntent { + status: attempt_status, + updated_by: storage_scheme.to_string(), + connector, + merchant_connector_id, + authentication_type, + }; + + let updated_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent.clone(), + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment intent")?; + + payment_data.payment_intent = updated_payment_intent; + + let updated_payment_attempt = db + .update_payment_attempt( + key_manager_state, + key_store, + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment attempt")?; + + payment_data.payment_attempt = updated_payment_attempt; + + Ok((Box::new(self), payment_data)) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl PostUpdateTracker, types::PaymentsAuthorizeData> + for PaymentProxyIntent +{ + async fn update_tracker<'b>( + &'b self, + state: &'b SessionState, + mut payment_data: PaymentConfirmData, + response: types::RouterData, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send + Sync, + types::RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< + F, + types::PaymentsAuthorizeData, + PaymentConfirmData, + >, + { + use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + + let db = &*state.store; + let key_manager_state = &state.into(); + + let response_router_data = response; + + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); + let payment_attempt_update = + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); + + let updated_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment intent")?; + + let updated_payment_attempt = db + .update_payment_attempt( + key_manager_state, + key_store, + payment_data.payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment attempt")?; + + payment_data.payment_intent = updated_payment_intent; + payment_data.payment_attempt = updated_payment_attempt; + + Ok(payment_data) + } +} diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 661bd73b506..1a9ced82ff4 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -257,14 +257,13 @@ pub async fn construct_payment_router_data_for_authorize<'a>( .browser_info .clone() .map(types::BrowserInformation::from); - // TODO: few fields are repeated in both routerdata and request let request = types::PaymentsAuthorizeData { payment_method_data: payment_data .payment_method_data .get_required_value("payment_method_data")?, setup_future_usage: Some(payment_data.payment_intent.setup_future_usage), - mandate_id: None, + mandate_id: payment_data.mandate_data.clone(), off_session: None, setup_mandate_details: None, confirm: true, @@ -315,6 +314,16 @@ pub async fn construct_payment_router_data_for_authorize<'a>( .as_ref() .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + let connector_customer_id = payment_data + .payment_intent + .feature_metadata + .as_ref() + .and_then(|fm| fm.payment_revenue_recovery_metadata.as_ref()) + .map(|rrm| { + rrm.billing_connector_payment_details + .connector_customer_id + .clone() + }); // TODO: evaluate the fields in router data, if they are required or not let router_data = types::RouterData { flow: PhantomData, @@ -1674,6 +1683,94 @@ where } } +#[cfg(feature = "v2")] +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentConfirmData +where + F: Clone, +{ + fn generate_response( + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + let payment_intent = self.payment_intent; + let payment_attempt = self.payment_attempt; + + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( + &payment_intent.amount_details, + &payment_attempt.amount_details, + )); + + let connector = payment_attempt + .connector + .clone() + .get_required_value("connector") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Connector is none when constructing response")?; + + let merchant_connector_id = payment_attempt + .merchant_connector_id + .clone() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant connector id is none when constructing response")?; + + let error = payment_attempt.error.clone().map( + |from: hyperswitch_domain_models::payments::payment_attempt::ErrorDetails| { + api_models::payments::ErrorDetails::foreign_from(&from) + }, + ); + let payment_address = self.payment_address; + + let payment_method_data = + Some(api_models::payments::PaymentMethodDataResponseWithBilling { + payment_method_data: None, + billing: payment_address + .get_request_payment_method_billing() + .cloned() + .map(From::from), + }); + + // TODO: Add support for other next actions, currently only supporting redirect to url + let redirect_to_url = payment_intent.create_start_redirection_url( + &state.base_url, + merchant_account.publishable_key.clone(), + )?; + + let next_action = payment_attempt + .redirection_data + .as_ref() + .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); + + let response = api_models::payments::ProxyPaymentsIntentResponse { + id: payment_intent.id.clone(), + status: payment_intent.status, + amount, + connector, + client_secret: payment_intent.client_secret.clone(), + created: payment_intent.created_at, + payment_method_data, + payment_method_type: payment_attempt.payment_method_type, + payment_method_subtype: payment_attempt.payment_method_subtype, + next_action, + connector_transaction_id: payment_attempt.connector_payment_id.clone(), + connector_reference_id: None, + merchant_connector_id, + browser_info: None, + error, + }; + + Ok(services::ApplicationResponse::JsonWithHeaders(( + response, + vec![], + ))) + } +} + #[cfg(feature = "v2")] impl GenerateResponse for hyperswitch_domain_models::payments::PaymentStatusData diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index fbf72d8a548..31c94d9a42d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -596,6 +596,10 @@ impl Payments { web::resource("/confirm-intent") .route(web::post().to(payments::payment_confirm_intent)), ) + .service( + web::resource("/proxy-confirm-intent") + .route(web::post().to(payments::proxy_confirm_intent)), + ) .service( web::resource("/get-intent") .route(web::get().to(payments::payments_get_intent)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index f48a7970fde..0f45b352c7f 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -150,6 +150,7 @@ impl From for ApiIdentifier { | Flow::PaymentsUpdateIntent | Flow::PaymentsCreateAndConfirmIntent | Flow::PaymentStartRedirection + | Flow::ProxyConfirmIntent | Flow::PaymentsRetrieveUsingMerchantReferenceId => Self::Payments, Flow::PayoutsCreate diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 870bd63f010..35b1edc11d6 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -3,7 +3,6 @@ use crate::{ services::authorization::permissions::Permission, }; pub mod helpers; - use actix_web::{web, Responder}; use error_stack::report; use hyperswitch_domain_models::payments::HeaderPayload; @@ -2452,6 +2451,78 @@ pub async fn payment_confirm_intent( .await } +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::ProxyConfirmIntent, payment_id))] +pub async fn proxy_confirm_intent( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + use hyperswitch_domain_models::payments::PaymentConfirmData; + + let flow = Flow::ProxyConfirmIntent; + + // Extract the payment ID from the path + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + // Parse and validate headers + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + // Prepare the internal payload + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id, + payload: json_payload.into_inner(), + }; + + // Determine the locking action, if required + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, req_state| { + let payment_id = req.global_payment_id; + let request = req.payload; + + // Define the operation for proxy payments intent + let operation = payments::operations::proxy_payments_intent::PaymentProxyIntent; + + // Call the core proxy logic + Box::pin(payments::proxy_for_payments_core::< + api_types::Authorize, + api_models::payments::ProxyPaymentsIntentResponse, + _, + _, + _, + PaymentConfirmData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + operation, + request, + payment_id, + payments::CallConnectorAction::Trigger, + header_payload.clone(), + )) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + locking_action, + )) + .await +} + #[cfg(feature = "v2")] #[instrument(skip(state, req), fields(flow, payment_id))] pub async fn payment_status( diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 6021ee70d9f..4755cacf6a3 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -533,6 +533,7 @@ pub enum Flow { PaymentsManualUpdate, /// Dynamic Tax Calcultion SessionUpdateTaxCalculation, + ProxyConfirmIntent, /// Payments post session tokens flow PaymentsPostSessionTokens, /// Payments start redirection flow