diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index 5bcf00dbf2..8e9015257d 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -192,7 +192,7 @@ jobs: CONNECTOR_AUTH_PASSPHRASE: ${{ secrets.CONNECTOR_AUTH_PASSPHRASE }} CONNECTOR_CREDS_S3_BUCKET_URI: ${{ secrets.CONNECTOR_CREDS_S3_BUCKET_URI}} DESTINATION_FILE_NAME: "creds.json.gpg" - S3_SOURCE_FILE_NAME: "6859bf7e-735b-4589-979a-ac057ed50425.json.gpg" + S3_SOURCE_FILE_NAME: "9f00461f-5a48-4f9c-aa8c-3510e4cb7d9c.json.gpg" shell: bash run: | mkdir -p ".github/secrets" ".github/test" diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 02dbdddf4a..390be5f86f 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -22770,8 +22770,7 @@ "type": "string", "enum": [ "true", - "false", - "default" + "false" ] }, "RequestPaymentMethodTypes": { diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 32b6cdf411..a895f0710c 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2848,9 +2848,8 @@ pub enum CountryAlpha2 { #[strum(serialize_all = "snake_case")] pub enum RequestIncrementalAuthorization { True, - False, #[default] - Default, + False, } #[derive(Clone, Copy, Eq, Hash, PartialEq, Debug, Serialize, Deserialize, strum::Display, ToSchema,)] diff --git a/crates/hyperswitch_connectors/src/connectors/stripe.rs b/crates/hyperswitch_connectors/src/connectors/stripe.rs index a7146fab9c..ee9e3f86f6 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripe.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripe.rs @@ -22,15 +22,16 @@ use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ - AccessTokenAuth, Authorize, Capture, CreateConnectorCustomer, Evidence, Execute, PSync, - PaymentMethodToken, RSync, Retrieve, Session, SetupMandate, UpdateMetadata, Upload, Void, + AccessTokenAuth, Authorize, Capture, CreateConnectorCustomer, Evidence, Execute, + IncrementalAuthorization, PSync, PaymentMethodToken, RSync, Retrieve, Session, + SetupMandate, UpdateMetadata, Upload, Void, }, router_request_types::{ AccessTokenRequestData, ConnectorCustomerData, PaymentMethodTokenizationData, - PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, - PaymentsSyncData, PaymentsUpdateMetadataData, RefundsData, RetrieveFileRequestData, - SetupMandateRequestData, SplitRefundsRequest, SubmitEvidenceRequestData, - UploadFileRequestData, + PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, + PaymentsIncrementalAuthorizationData, PaymentsSessionData, PaymentsSyncData, + PaymentsUpdateMetadataData, RefundsData, RetrieveFileRequestData, SetupMandateRequestData, + SplitRefundsRequest, SubmitEvidenceRequestData, UploadFileRequestData, }, router_response_types::{ PaymentsResponseData, RefundsResponseData, RetrieveFileResponse, SubmitEvidenceResponse, @@ -38,8 +39,9 @@ use hyperswitch_domain_models::{ }, types::{ ConnectorCustomerRouterData, PaymentsAuthorizeRouterData, PaymentsCancelRouterData, - PaymentsCaptureRouterData, PaymentsSyncRouterData, PaymentsUpdateMetadataRouterData, - RefundsRouterData, TokenizationRouterData, + PaymentsCaptureRouterData, PaymentsIncrementalAuthorizationRouterData, + PaymentsSyncRouterData, PaymentsUpdateMetadataRouterData, RefundsRouterData, + TokenizationRouterData, }, }; #[cfg(feature = "payouts")] @@ -58,7 +60,7 @@ use hyperswitch_interfaces::{ disputes::SubmitEvidence, files::{FilePurpose, FileUpload, RetrieveFile, UploadFile}, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorRedirectResponse, - ConnectorSpecifications, ConnectorValidation, + ConnectorSpecifications, ConnectorValidation, PaymentIncrementalAuthorization, }, configs::Connectors, consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}, @@ -66,9 +68,10 @@ use hyperswitch_interfaces::{ errors::ConnectorError, events::connector_api_logs::ConnectorEvent, types::{ - ConnectorCustomerType, PaymentsAuthorizeType, PaymentsCaptureType, PaymentsSyncType, - PaymentsUpdateMetadataType, PaymentsVoidType, RefundExecuteType, RefundSyncType, Response, - RetrieveFileType, SubmitEvidenceType, TokenizationType, UploadFileType, + ConnectorCustomerType, IncrementalAuthorizationType, PaymentsAuthorizeType, + PaymentsCaptureType, PaymentsSyncType, PaymentsUpdateMetadataType, PaymentsVoidType, + RefundExecuteType, RefundSyncType, Response, RetrieveFileType, SubmitEvidenceType, + TokenizationType, UploadFileType, }, webhooks::{IncomingWebhook, IncomingWebhookRequestDetails}, }; @@ -1030,6 +1033,150 @@ impl ConnectorIntegration for Stripe +{ + fn get_headers( + &self, + req: &PaymentsIncrementalAuthorizationRouterData, + connectors: &Connectors, + ) -> CustomResult)>, ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_http_method(&self) -> Method { + Method::Post + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &PaymentsIncrementalAuthorizationRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}v1/payment_intents/{}/increment_authorization", + self.base_url(connectors), + req.request.connector_transaction_id, + )) + } + + fn get_request_body( + &self, + req: &PaymentsIncrementalAuthorizationRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount = utils::convert_amount( + self.amount_converter, + MinorUnit::new(req.request.total_amount), + req.request.currency, + )?; + let connector_req = stripe::StripeIncrementalAuthRequest { amount }; + + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsIncrementalAuthorizationRouterData, + connectors: &Connectors, + ) -> CustomResult, ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&IncrementalAuthorizationType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(IncrementalAuthorizationType::get_headers( + self, req, connectors, + )?) + .set_body(IncrementalAuthorizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsIncrementalAuthorizationRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult< + RouterData< + IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, + >, + ConnectorError, + > { + let response: stripe::PaymentIntentResponse = res + .response + .parse_struct("PaymentIntentResponse") + .change_context(ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: stripe::ErrorResponse = res + .response + .parse_struct("ErrorResponse") + .change_context(ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_error_response_body(&response)); + router_env::logger::info!(connector_response=?response); + Ok(ErrorResponse { + status_code: res.status_code, + code: response + .error + .code + .clone() + .unwrap_or_else(|| NO_ERROR_CODE.to_string()), + message: response + .error + .code + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + reason: response.error.message.map(|message| { + response + .error + .decline_code + .clone() + .map(|decline_code| { + format!("message - {message}, decline_code - {decline_code}") + }) + .unwrap_or(message) + }), + attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), + }) + } +} + impl ConnectorIntegration for Stripe { diff --git a/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs index b1ee1ef2cc..71448ef817 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripe/transformers.rs @@ -24,7 +24,8 @@ use hyperswitch_domain_models::{ router_flow_types::{Execute, RSync}, router_request_types::{ BrowserInformation, ChargeRefundsOptions, DestinationChargeRefund, DirectChargeRefund, - ResponseId, SplitRefundsRequest, + PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, + PaymentsIncrementalAuthorizationData, ResponseId, SplitRefundsRequest, }, router_response_types::{ MandateReference, PaymentsResponseData, PreprocessingResponseId, RedirectForm, @@ -66,6 +67,28 @@ pub mod auth_headers { pub const STRIPE_VERSION: &str = "2022-11-15"; } +trait GetRequestIncrementalAuthorization { + fn get_request_incremental_authorization(&self) -> Option; +} + +impl GetRequestIncrementalAuthorization for PaymentsAuthorizeData { + fn get_request_incremental_authorization(&self) -> Option { + Some(self.request_incremental_authorization) + } +} + +impl GetRequestIncrementalAuthorization for PaymentsCaptureData { + fn get_request_incremental_authorization(&self) -> Option { + None + } +} + +impl GetRequestIncrementalAuthorization for PaymentsCancelData { + fn get_request_incremental_authorization(&self) -> Option { + None + } +} + pub struct StripeAuthType { pub(super) api_key: Secret, } @@ -257,7 +280,18 @@ pub struct StripeCardData { pub payment_method_auth_type: Option, #[serde(rename = "payment_method_options[card][network]")] pub payment_method_data_card_preferred_network: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "payment_method_options[card][request_incremental_authorization]")] + pub request_incremental_authorization: Option, } + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum StripeRequestIncrementalAuthorization { + IfAvailable, + Never, +} + #[derive(Debug, Eq, PartialEq, Serialize)] pub struct StripePayLaterData { #[serde(rename = "payment_method_data[type]")] @@ -1219,6 +1253,7 @@ fn create_stripe_payment_method( payment_method_token: Option, is_customer_initiated_mandate_payment: Option, billing_address: StripeBillingAddress, + request_incremental_authorization: bool, ) -> Result< ( StripePaymentMethodData, @@ -1234,7 +1269,11 @@ fn create_stripe_payment_method( enums::AuthenticationType::NoThreeDs => Auth3ds::Automatic, }; Ok(( - StripePaymentMethodData::try_from((card_details, payment_method_auth_type))?, + StripePaymentMethodData::try_from(( + card_details, + payment_method_auth_type, + request_incremental_authorization, + ))?, Some(StripePaymentMethodType::Card), billing_address, )) @@ -1452,9 +1491,11 @@ fn get_stripe_card_network(card_network: common_enums::CardNetwork) -> Option for StripePaymentMethodData { +impl TryFrom<(&Card, Auth3ds, bool)> for StripePaymentMethodData { type Error = ConnectorError; - fn try_from((card, payment_method_auth_type): (&Card, Auth3ds)) -> Result { + fn try_from( + (card, payment_method_auth_type, request_incremental_authorization): (&Card, Auth3ds, bool), + ) -> Result { Ok(Self::Card(StripeCardData { payment_method_data_type: StripePaymentMethodType::Card, payment_method_data_card_number: card.card_number.clone(), @@ -1466,6 +1507,11 @@ impl TryFrom<(&Card, Auth3ds)> for StripePaymentMethodData { .card_network .clone() .and_then(get_stripe_card_network), + request_incremental_authorization: if request_incremental_authorization { + Some(StripeRequestIncrementalAuthorization::IfAvailable) + } else { + None + }, })) } } @@ -1814,6 +1860,7 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest .card_network .clone() .and_then(get_stripe_card_network), + request_incremental_authorization: None, }), PaymentMethodData::CardRedirect(_) | PaymentMethodData::Wallet(_) @@ -1862,6 +1909,7 @@ impl TryFrom<(&PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntentRequest field_name: "billing_address", } })?, + item.request.request_incremental_authorization, )?; validate_shipping_address_against_payment_method( @@ -2187,6 +2235,7 @@ impl TryFrom<&TokenizationRouterData> for TokenRequest { item.payment_method_token.clone(), None, StripeBillingAddress::default(), + false, )? .0 } @@ -2300,6 +2349,11 @@ impl TryFrom<&PaymentsAuthorizeRouterData> for StripeSplitPaymentRequest { } } +#[derive(Debug, Serialize)] +pub struct StripeIncrementalAuthRequest { + pub amount: MinorUnit, +} + #[derive(Clone, Default, Debug, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum StripePaymentStatus { @@ -2661,7 +2715,7 @@ fn extract_payment_method_connector_response_from_latest_attempt( impl TryFrom> for RouterData where - T: SplitPaymentData, + T: SplitPaymentData + GetRequestIncrementalAuthorization, { type Error = error_stack::Report; fn try_from( @@ -2743,7 +2797,10 @@ where connector_metadata, network_txn_id, connector_response_reference_id: Some(item.response.id), - incremental_authorization_allowed: None, + incremental_authorization_allowed: item + .data + .request + .get_request_incremental_authorization(), charges, }) }; @@ -2772,6 +2829,55 @@ where } } +impl From for common_enums::AuthorizationStatus { + fn from(item: StripePaymentStatus) -> Self { + match item { + StripePaymentStatus::Succeeded + | StripePaymentStatus::RequiresCapture + | StripePaymentStatus::Chargeable + | StripePaymentStatus::RequiresCustomerAction + | StripePaymentStatus::RequiresConfirmation + | StripePaymentStatus::Consumed => Self::Success, + StripePaymentStatus::Processing | StripePaymentStatus::Pending => Self::Processing, + StripePaymentStatus::Failed + | StripePaymentStatus::Canceled + | StripePaymentStatus::RequiresPaymentMethod => Self::Failure, + } + } +} + +impl + TryFrom< + ResponseRouterData< + F, + PaymentIntentResponse, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, + >, + > for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + PaymentIntentResponse, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, + >, + ) -> Result { + let status = common_enums::AuthorizationStatus::from(item.response.status); + Ok(Self { + response: Ok(PaymentsResponseData::IncrementalAuthorizationResponse { + status, + error_code: None, + error_message: None, + connector_authorization_id: Some(item.response.id), + }), + ..item.data + }) + } +} + pub fn get_connector_metadata( next_action: Option<&StripeNextActionResponse>, amount: MinorUnit, @@ -4041,7 +4147,11 @@ impl enums::AuthenticationType::ThreeDs => Auth3ds::Any, enums::AuthenticationType::NoThreeDs => Auth3ds::Automatic, }; - Ok(Self::try_from((ccard, payment_method_auth_type))?) + Ok(Self::try_from(( + ccard, + payment_method_auth_type, + item.request.request_incremental_authorization, + ))?) } PaymentMethodData::PayLater(_) => Ok(Self::PayLater(StripePayLaterData { payment_method_data_type: pm_type, diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 489ac49cf2..ce6bb59675 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -1350,7 +1350,6 @@ default_imp_for_incremental_authorization!( connectors::Signifyd, connectors::Stax, connectors::Square, - connectors::Stripe, connectors::Stripebilling, connectors::Taxjar, connectors::Threedsecureio, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 2cdbc13f99..e0173113a7 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -314,7 +314,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( payment_data .payment_intent .request_incremental_authorization, - RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + RequestIncrementalAuthorization::True ), metadata: payment_data.payment_intent.metadata.expose_option(), authentication_data: None, @@ -1025,7 +1025,7 @@ pub async fn construct_payment_router_data_for_setup_mandate<'a>( payment_data .payment_intent .request_incremental_authorization, - RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + RequestIncrementalAuthorization::True ), metadata: payment_data.payment_intent.metadata, minor_amount: Some(payment_data.payment_attempt.amount_details.get_net_amount()), @@ -3935,7 +3935,6 @@ impl TryFrom> for types::PaymentsAuthoriz .payment_intent .request_incremental_authorization, Some(RequestIncrementalAuthorization::True) - | Some(RequestIncrementalAuthorization::Default) ), metadata: additional_data.payment_data.payment_intent.metadata, authentication_data: payment_data @@ -4844,7 +4843,6 @@ impl TryFrom> for types::SetupMandateRequ .payment_intent .request_incremental_authorization, Some(RequestIncrementalAuthorization::True) - | Some(RequestIncrementalAuthorization::Default) ), metadata: payment_data.payment_intent.metadata.clone().map(Into::into), shipping_cost: payment_data.payment_intent.shipping_cost, diff --git a/cypress-tests/cypress/e2e/configs/Payment/Stripe.js b/cypress-tests/cypress/e2e/configs/Payment/Stripe.js index 777a0802bf..d55fa0a59e 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Stripe.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Stripe.js @@ -390,6 +390,27 @@ export const connectorDetails = { }, }, }, + // IncrementalAuth: { // commenting out due to credentials issue + // Request: { + // amount: 8000, + // }, + // Response: { + // status: 200, + // body: { + // status: "requires_capture", + // amount: 8000, + // amount_capturable: 8000, + // amount_received: null, + // incremental_authorizations: [ + // { + // amount: 8000, + // previously_authorized_amount: 6000, + // status: "requires_capture", + // }, + // ], + // }, + // }, + // }, MandateSingleUse3DSAutoCapture: { Request: { payment_method: "card", diff --git a/cypress-tests/cypress/e2e/configs/Payment/Utils.js b/cypress-tests/cypress/e2e/configs/Payment/Utils.js index 31a5513dbc..b73d8f24ed 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Utils.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Utils.js @@ -403,6 +403,7 @@ export const CONNECTOR_LISTS = { "archipel", // "cybersource", // issues with MULTIPLE_CONNECTORS handling "paypal", + // "stripe", ], DDC_RACE_CONDITION: ["worldpay"], // Add more inclusion lists