From 53e82c3faef3ee629a38180e3882a2920332a9a8 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:29:16 +0530 Subject: [PATCH] feat(core): add payments post_session_tokens flow (#6202) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 5 + .../payments--post-session-tokens.mdx | 3 + api-reference/mint.json | 3 +- api-reference/openapi_spec.json | 94 ++++++ crates/api_models/src/events/payment.rs | 19 +- crates/api_models/src/payments.rs | 31 ++ crates/diesel_models/src/payment_attempt.rs | 58 ++++ .../src/default_implementations.rs | 57 +++- .../src/default_implementations_v2.rs | 23 +- .../src/payments/payment_attempt.rs | 13 + .../src/router_flow_types/payments.rs | 3 + .../src/router_request_types.rs | 11 + crates/hyperswitch_domain_models/src/types.rs | 11 +- .../src/api/payments.rs | 14 +- .../src/api/payments_v2.rs | 19 +- crates/hyperswitch_interfaces/src/types.rs | 17 +- crates/openapi/src/openapi.rs | 3 + crates/openapi/src/routes/payments.rs | 18 ++ crates/router/src/configs/defaults.rs | 75 ++++- crates/router/src/connector/paypal.rs | 153 +++++++-- .../src/connector/paypal/transformers.rs | 195 ++++++++++-- crates/router/src/connector/utils.rs | 14 + crates/router/src/core/payments.rs | 5 +- .../connector_integration_v2_impls.rs | 19 ++ crates/router/src/core/payments/flows.rs | 82 +++++ .../flows/post_session_tokens_flow.rs | 131 ++++++++ .../src/core/payments/flows/session_flow.rs | 2 +- crates/router/src/core/payments/operations.rs | 7 +- .../operations/payment_post_session_tokens.rs | 291 ++++++++++++++++++ .../payments/operations/payment_response.rs | 71 ++++- .../router/src/core/payments/transformers.rs | 75 +++++ crates/router/src/routes/app.rs | 3 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/payments.rs | 75 +++++ crates/router/src/services/api.rs | 6 + crates/router/src/services/authentication.rs | 6 + crates/router/src/types.rs | 24 +- crates/router/src/types/api/payments.rs | 21 +- crates/router/src/types/api/payments_v2.rs | 8 +- crates/router_derive/src/macros/operation.rs | 10 + crates/router_env/src/logger/types.rs | 2 + 41 files changed, 1572 insertions(+), 108 deletions(-) create mode 100644 api-reference/api-reference/payments/payments--post-session-tokens.mdx create mode 100644 crates/router/src/core/payments/flows/post_session_tokens_flow.rs create mode 100644 crates/router/src/core/payments/operations/payment_post_session_tokens.rs diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 7e9cc57bf6aa..9ecfe9cf644b 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -10217,6 +10217,7 @@ "NextActionCall": { "type": "string", "enum": [ + "post_session_tokens", "confirm", "sync", "complete_authorize" @@ -19327,6 +19328,10 @@ "properties": { "next_action": { "$ref": "#/components/schemas/NextActionCall" + }, + "order_id": { + "type": "string", + "nullable": true } } }, diff --git a/api-reference/api-reference/payments/payments--post-session-tokens.mdx b/api-reference/api-reference/payments/payments--post-session-tokens.mdx new file mode 100644 index 000000000000..6c327886c529 --- /dev/null +++ b/api-reference/api-reference/payments/payments--post-session-tokens.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /payments/{payment_id}/post_session_tokens +--- \ No newline at end of file diff --git a/api-reference/mint.json b/api-reference/mint.json index 03a76fed1830..264f5366836b 100644 --- a/api-reference/mint.json +++ b/api-reference/mint.json @@ -52,7 +52,8 @@ "api-reference/payments/payments-link--retrieve", "api-reference/payments/payments--list", "api-reference/payments/payments--external-3ds-authentication", - "api-reference/payments/payments--complete-authorize" + "api-reference/payments/payments--complete-authorize", + "api-reference/payments/post-session-tokens" ] }, { diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d01a51f6d5b9..49fe3b6b95d2 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -912,6 +912,46 @@ ] } }, + "/payments/{payment_id}/post_session_tokens": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Post Session Tokens", + "description": "\n", + "operationId": "Create Post Session Tokens for a Payment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsPostSessionTokensRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Post Session Token is done", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsPostSessionTokensResponse" + } + } + } + }, + "400": { + "description": "Missing mandatory fields" + } + }, + "security": [ + { + "publishable_key": [] + } + ] + } + }, "/refunds": { "post": { "tags": [ @@ -13811,6 +13851,7 @@ "NextActionCall": { "type": "string", "enum": [ + "post_session_tokens", "confirm", "sync", "complete_authorize" @@ -17928,6 +17969,55 @@ } } }, + "PaymentsPostSessionTokensRequest": { + "type": "object", + "required": [ + "client_secret", + "payment_method_type", + "payment_method" + ], + "properties": { + "client_secret": { + "type": "string", + "description": "It's a token used for client side verification." + }, + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + } + } + }, + "PaymentsPostSessionTokensResponse": { + "type": "object", + "required": [ + "payment_id", + "status" + ], + "properties": { + "payment_id": { + "type": "string", + "description": "The identifier for the payment" + }, + "next_action": { + "allOf": [ + { + "$ref": "#/components/schemas/NextActionData" + } + ], + "nullable": true + }, + "status": { + "allOf": [ + { + "$ref": "#/components/schemas/IntentStatus" + } + ], + "default": "requires_confirmation" + } + } + }, "PaymentsRequest": { "type": "object", "properties": { @@ -23039,6 +23129,10 @@ "properties": { "next_action": { "$ref": "#/components/schemas/NextActionCall" + }, + "order_id": { + "type": "string", + "nullable": true } } }, diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index aa8cd43ca97f..1f66fb521e4e 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -23,7 +23,8 @@ use crate::{ PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest, - PaymentsManualUpdateRequest, PaymentsManualUpdateResponse, PaymentsRejectRequest, + PaymentsManualUpdateRequest, PaymentsManualUpdateResponse, + PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, PaymentsSessionResponse, PaymentsStartRequest, RedirectionResponse, }, @@ -71,6 +72,22 @@ impl ApiEventMetric for PaymentsDynamicTaxCalculationRequest { } } +impl ApiEventMetric for PaymentsPostSessionTokensRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +impl ApiEventMetric for PaymentsPostSessionTokensResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + impl ApiEventMetric for PaymentsDynamicTaxCalculationResponse {} impl ApiEventMetric for PaymentsCancelRequest { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 87152c5c1704..265865b973ae 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3819,6 +3819,7 @@ pub enum QrCodeInformation { #[serde(rename_all = "snake_case")] pub struct SdkNextActionData { pub next_action: NextActionCall, + pub order_id: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -4873,6 +4874,34 @@ pub struct PaymentsSessionRequest { pub merchant_connector_details: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct PaymentsPostSessionTokensRequest { + /// The unique identifier for the payment + #[serde(skip_deserializing)] + #[schema(value_type = String)] + pub payment_id: id_type::PaymentId, + /// It's a token used for client side verification. + #[schema(value_type = String)] + pub client_secret: Secret, + /// Payment method type + #[schema(value_type = PaymentMethodType)] + pub payment_method_type: api_enums::PaymentMethodType, + /// The payment method that is to be used for the payment + #[schema(value_type = PaymentMethod, example = "card")] + pub payment_method: api_enums::PaymentMethod, +} + +#[derive(Debug, serde::Serialize, Clone, ToSchema)] +pub struct PaymentsPostSessionTokensResponse { + /// The identifier for the payment + #[schema(value_type = String)] + pub payment_id: id_type::PaymentId, + /// Additional information required for redirection + pub next_action: Option, + #[schema(value_type = IntentStatus, example = "failed", default = "requires_confirmation")] + pub status: api_enums::IntentStatus, +} + #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] pub struct PaymentsDynamicTaxCalculationRequest { /// The unique identifier for the payment @@ -5429,6 +5458,8 @@ pub struct SdkNextAction { #[derive(Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Clone, ToSchema)] #[serde(rename_all = "snake_case")] pub enum NextActionCall { + /// The next action call is Post Session Tokens + PostSessionTokens, /// The next action call is confirm Confirm, /// The next action call is sync diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 061f00f8295e..20551c2754ba 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -475,6 +475,10 @@ pub enum PaymentAttemptUpdate { unified_message: Option, connector_transaction_id: Option, }, + PostSessionTokensUpdate { + updated_by: String, + connector_metadata: Option, + }, } #[cfg(feature = "v2")] @@ -3065,6 +3069,60 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost: None, order_tax_amount: None, }, + PaymentAttemptUpdate::PostSessionTokensUpdate { + updated_by, + connector_metadata, + } => Self { + status: None, + error_code: None, + modified_at: common_utils::date_time::now(), + error_message: None, + error_reason: None, + updated_by, + unified_code: None, + unified_message: None, + amount: None, + net_amount: None, + currency: None, + connector_transaction_id: None, + amount_to_capture: None, + connector: None, + authentication_type: None, + payment_method: None, + payment_method_id: None, + cancellation_reason: None, + mandate_id: None, + browser_info: None, + payment_token: None, + connector_metadata, + payment_method_data: None, + payment_method_type: None, + payment_experience: None, + business_sub_label: None, + straight_through_algorithm: None, + preprocessing_step_id: None, + capture_method: None, + connector_response_reference_id: None, + multiple_capture_count: None, + surcharge_amount: None, + tax_amount: None, + amount_capturable: None, + merchant_connector_id: None, + authentication_data: None, + encoded_data: None, + external_three_ds_authentication_attempted: None, + authentication_connector: None, + authentication_id: None, + fingerprint_id: None, + payment_method_billing_address_id: None, + charge_id: None, + client_source: None, + client_version: None, + customer_acceptance: None, + card_network: None, + shipping_cost: None, + order_tax_amount: None, + }, } } } diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 7600b08fe1ed..8a9d803a6f54 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -29,8 +29,8 @@ use hyperswitch_domain_models::{ mandate_revoke::MandateRevoke, payments::{ Approve, AuthorizeSessionToken, CalculateTax, CompleteAuthorize, - CreateConnectorCustomer, IncrementalAuthorization, PostProcessing, PreProcessing, - Reject, SdkSessionUpdate, + CreateConnectorCustomer, IncrementalAuthorization, PostProcessing, PostSessionTokens, + PreProcessing, Reject, SdkSessionUpdate, }, webhooks::VerifyWebhookSource, }, @@ -38,9 +38,9 @@ use hyperswitch_domain_models::{ AcceptDisputeRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, DefendDisputeRequestData, MandateRevokeRequestData, PaymentsApproveData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, - PaymentsPreProcessingData, PaymentsRejectData, PaymentsTaxCalculationData, - RetrieveFileRequestData, SdkPaymentsSessionUpdateData, SubmitEvidenceRequestData, - UploadFileRequestData, VerifyWebhookSourceRequestData, + PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsRejectData, + PaymentsTaxCalculationData, RetrieveFileRequestData, SdkPaymentsSessionUpdateData, + SubmitEvidenceRequestData, UploadFileRequestData, VerifyWebhookSourceRequestData, }, router_response_types::{ AcceptDisputeResponse, DefendDisputeResponse, MandateRevokeResponseData, @@ -65,9 +65,9 @@ use hyperswitch_interfaces::{ files::{FileUpload, RetrieveFile, UploadFile}, payments::{ ConnectorCustomer, PaymentApprove, PaymentAuthorizeSessionToken, - PaymentIncrementalAuthorization, PaymentReject, PaymentSessionUpdate, - PaymentsCompleteAuthorize, PaymentsPostProcessing, PaymentsPreProcessing, - TaxCalculation, + PaymentIncrementalAuthorization, PaymentPostSessionTokens, PaymentReject, + PaymentSessionUpdate, PaymentsCompleteAuthorize, PaymentsPostProcessing, + PaymentsPreProcessing, TaxCalculation, }, ConnectorIntegration, ConnectorMandateRevoke, ConnectorRedirectResponse, }, @@ -195,6 +195,47 @@ default_imp_for_session_update!( connectors::Volt ); +macro_rules! default_imp_for_post_session_tokens { + ($($path:ident::$connector:ident),*) => { + $( impl PaymentPostSessionTokens for $path::$connector {} + impl + ConnectorIntegration< + PostSessionTokens, + PaymentsPostSessionTokensData, + PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +default_imp_for_post_session_tokens!( + connectors::Bambora, + connectors::Bitpay, + connectors::Cashtocode, + connectors::Coinbase, + connectors::Cryptopay, + connectors::Digitalvirgo, + connectors::Dlocal, + connectors::Square, + connectors::Fiserv, + connectors::Fiservemea, + connectors::Helcim, + connectors::Stax, + connectors::Taxjar, + connectors::Mollie, + connectors::Novalnet, + connectors::Nexixpay, + connectors::Fiuu, + connectors::Globepay, + connectors::Worldline, + connectors::Powertranz, + connectors::Thunes, + connectors::Tsys, + connectors::Deutschebank, + connectors::Volt +); + use crate::connectors; macro_rules! default_imp_for_complete_authorize { ($($path:ident::$connector:ident),*) => { diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index c1170709c021..1c81db29d3fe 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -14,7 +14,8 @@ use hyperswitch_domain_models::{ payments::{ Approve, Authorize, AuthorizeSessionToken, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, PSync, PaymentMethodToken, - PostProcessing, PreProcessing, Reject, SdkSessionUpdate, Session, SetupMandate, Void, + PostProcessing, PostSessionTokens, PreProcessing, Reject, SdkSessionUpdate, Session, + SetupMandate, Void, }, refunds::{Execute, RSync}, webhooks::VerifyWebhookSource, @@ -26,10 +27,10 @@ use hyperswitch_domain_models::{ MandateRevokeRequestData, PaymentMethodTokenizationData, PaymentsApproveData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, - PaymentsPreProcessingData, PaymentsRejectData, PaymentsSessionData, PaymentsSyncData, - PaymentsTaxCalculationData, RefundsData, RetrieveFileRequestData, - SdkPaymentsSessionUpdateData, SetupMandateRequestData, SubmitEvidenceRequestData, - UploadFileRequestData, VerifyWebhookSourceRequestData, + PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsRejectData, + PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, RefundsData, + RetrieveFileRequestData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, + SubmitEvidenceRequestData, UploadFileRequestData, VerifyWebhookSourceRequestData, }, router_response_types::{ AcceptDisputeResponse, DefendDisputeResponse, MandateRevokeResponseData, @@ -74,8 +75,8 @@ use hyperswitch_interfaces::{ payments_v2::{ ConnectorCustomerV2, MandateSetupV2, PaymentApproveV2, PaymentAuthorizeSessionTokenV2, PaymentAuthorizeV2, PaymentCaptureV2, PaymentIncrementalAuthorizationV2, - PaymentRejectV2, PaymentSessionUpdateV2, PaymentSessionV2, PaymentSyncV2, - PaymentTokenV2, PaymentV2, PaymentVoidV2, PaymentsCompleteAuthorizeV2, + PaymentPostSessionTokensV2, PaymentRejectV2, PaymentSessionUpdateV2, PaymentSessionV2, + PaymentSyncV2, PaymentTokenV2, PaymentV2, PaymentVoidV2, PaymentsCompleteAuthorizeV2, PaymentsPostProcessingV2, PaymentsPreProcessingV2, TaxCalculationV2, }, refunds_v2::{RefundExecuteV2, RefundSyncV2, RefundV2}, @@ -107,6 +108,7 @@ macro_rules! default_imp_for_new_connector_integration_payment { impl PaymentsPostProcessingV2 for $path::$connector{} impl TaxCalculationV2 for $path::$connector{} impl PaymentSessionUpdateV2 for $path::$connector{} + impl PaymentPostSessionTokensV2 for $path::$connector{} impl ConnectorIntegrationV2 for $path::$connector{} @@ -191,6 +193,13 @@ macro_rules! default_imp_for_new_connector_integration_payment { SdkPaymentsSessionUpdateData, PaymentsResponseData, > for $path::$connector{} + impl + ConnectorIntegrationV2< + PostSessionTokens, + PaymentFlowData, + PaymentsPostSessionTokensData, + PaymentsResponseData, + > for $path::$connector{} )* }; } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 31423c33d23a..5aba06161435 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -556,6 +556,8 @@ pub struct PaymentAttemptNew { pub profile_id: id_type::ProfileId, pub organization_id: id_type::OrganizationId, pub card_network: Option, + pub shipping_cost: Option, + pub order_tax_amount: Option, } #[cfg(feature = "v1")] @@ -809,6 +811,10 @@ pub enum PaymentAttemptUpdate { unified_message: Option, connector_transaction_id: Option, }, + PostSessionTokensUpdate { + updated_by: String, + connector_metadata: Option, + }, } #[cfg(feature = "v1")] @@ -1160,6 +1166,13 @@ impl PaymentAttemptUpdate { unified_message, connector_transaction_id, }, + Self::PostSessionTokensUpdate { + updated_by, + connector_metadata, + } => DieselPaymentAttemptUpdate::PostSessionTokensUpdate { + updated_by, + connector_metadata, + }, } } } diff --git a/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs b/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs index a0e6adc855a9..10153fc05ec7 100644 --- a/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs +++ b/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs @@ -55,3 +55,6 @@ pub struct CalculateTax; #[derive(Debug, Clone)] pub struct SdkSessionUpdate; + +#[derive(Debug, Clone)] +pub struct PostSessionTokens; diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index b67621e9f069..f03ab75af19d 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -73,6 +73,17 @@ pub struct PaymentsAuthorizeData { pub integrity_object: Option, } +#[derive(Debug, Clone)] +pub struct PaymentsPostSessionTokensData { + pub amount: MinorUnit, + pub currency: storage_enums::Currency, + pub capture_method: Option, + /// Merchant's identifier for the payment/invoice. This will be sent to the connector + /// if the connector provides support to accept multiple reference ids. + /// In case the connector supports only one reference id, Hyperswitch's Payment ID will be sent as reference. + pub merchant_order_reference_id: Option, +} + #[derive(Debug, Clone, PartialEq)] pub struct AuthoriseIntegrityObject { /// Authorise amount diff --git a/crates/hyperswitch_domain_models/src/types.rs b/crates/hyperswitch_domain_models/src/types.rs index 507249e5ee26..eeb63bbb508f 100644 --- a/crates/hyperswitch_domain_models/src/types.rs +++ b/crates/hyperswitch_domain_models/src/types.rs @@ -2,14 +2,15 @@ use crate::{ router_data::{AccessToken, RouterData}, router_flow_types::{ AccessTokenAuth, Authorize, AuthorizeSessionToken, CalculateTax, Capture, - CompleteAuthorize, CreateConnectorCustomer, PSync, PaymentMethodToken, PreProcessing, - RSync, SetupMandate, Void, + CompleteAuthorize, CreateConnectorCustomer, PSync, PaymentMethodToken, PostSessionTokens, + PreProcessing, RSync, SetupMandate, Void, }, router_request_types::{ AccessTokenRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, PaymentMethodTokenizationData, PaymentsAuthorizeData, - PaymentsCancelData, PaymentsCaptureData, PaymentsPreProcessingData, PaymentsSyncData, - PaymentsTaxCalculationData, RefundsData, SetupMandateRequestData, + PaymentsCancelData, PaymentsCaptureData, PaymentsPostSessionTokensData, + PaymentsPreProcessingData, PaymentsSyncData, PaymentsTaxCalculationData, RefundsData, + SetupMandateRequestData, }, router_response_types::{ PaymentsResponseData, RefundsResponseData, TaxCalculationResponseData, @@ -38,3 +39,5 @@ pub type PaymentsCompleteAuthorizeRouterData = pub type PaymentsTaxCalculationRouterData = RouterData; pub type RefreshTokenRouterData = RouterData; +pub type PaymentsPostSessionTokensRouterData = + RouterData; diff --git a/crates/hyperswitch_interfaces/src/api/payments.rs b/crates/hyperswitch_interfaces/src/api/payments.rs index 43409680dc55..b3ac7e0fa906 100644 --- a/crates/hyperswitch_interfaces/src/api/payments.rs +++ b/crates/hyperswitch_interfaces/src/api/payments.rs @@ -4,14 +4,15 @@ use hyperswitch_domain_models::{ router_flow_types::payments::{ Approve, Authorize, AuthorizeSessionToken, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, PSync, PaymentMethodToken, - PostProcessing, PreProcessing, Reject, SdkSessionUpdate, Session, SetupMandate, Void, + PostProcessing, PostSessionTokens, PreProcessing, Reject, SdkSessionUpdate, Session, + SetupMandate, Void, }, router_request_types::{ AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, PaymentMethodTokenizationData, PaymentsApproveData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, - PaymentsPostProcessingData, PaymentsPreProcessingData, PaymentsRejectData, - PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, + PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, + PaymentsRejectData, PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, }, router_response_types::{PaymentsResponseData, TaxCalculationResponseData}, @@ -39,6 +40,7 @@ pub trait Payment: + ConnectorCustomer + PaymentIncrementalAuthorization + PaymentSessionUpdate + + PaymentPostSessionTokens { } @@ -124,6 +126,12 @@ pub trait PaymentSessionUpdate: { } +/// trait PostSessionTokens +pub trait PaymentPostSessionTokens: + api::ConnectorIntegration +{ +} + /// trait PaymentsCompleteAuthorize pub trait PaymentsCompleteAuthorize: api::ConnectorIntegration diff --git a/crates/hyperswitch_interfaces/src/api/payments_v2.rs b/crates/hyperswitch_interfaces/src/api/payments_v2.rs index cc5fd01f9f87..e2ddf49e0cd0 100644 --- a/crates/hyperswitch_interfaces/src/api/payments_v2.rs +++ b/crates/hyperswitch_interfaces/src/api/payments_v2.rs @@ -5,14 +5,15 @@ use hyperswitch_domain_models::{ router_flow_types::payments::{ Approve, Authorize, AuthorizeSessionToken, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, PSync, PaymentMethodToken, - PostProcessing, PreProcessing, Reject, SdkSessionUpdate, Session, SetupMandate, Void, + PostProcessing, PostSessionTokens, PreProcessing, Reject, SdkSessionUpdate, Session, + SetupMandate, Void, }, router_request_types::{ AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, PaymentMethodTokenizationData, PaymentsApproveData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, - PaymentsPostProcessingData, PaymentsPreProcessingData, PaymentsRejectData, - PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, + PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, + PaymentsRejectData, PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, }, router_response_types::{PaymentsResponseData, TaxCalculationResponseData}, @@ -112,6 +113,17 @@ pub trait PaymentSessionUpdateV2: { } +///trait PaymentPostSessionTokensV2 +pub trait PaymentPostSessionTokensV2: + ConnectorIntegrationV2< + PostSessionTokens, + PaymentFlowData, + PaymentsPostSessionTokensData, + PaymentsResponseData, +> +{ +} + /// trait PaymentsCompleteAuthorizeV2 pub trait PaymentsCompleteAuthorizeV2: ConnectorIntegrationV2< @@ -188,5 +200,6 @@ pub trait PaymentV2: + PaymentIncrementalAuthorizationV2 + TaxCalculationV2 + PaymentSessionUpdateV2 + + PaymentPostSessionTokensV2 { } diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs index e42942a94de9..81e2086f9721 100644 --- a/crates/hyperswitch_interfaces/src/types.rs +++ b/crates/hyperswitch_interfaces/src/types.rs @@ -9,7 +9,8 @@ use hyperswitch_domain_models::{ payments::{ Authorize, AuthorizeSessionToken, Balance, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, InitPayment, PSync, - PaymentMethodToken, PostProcessing, PreProcessing, Session, SetupMandate, Void, + PaymentMethodToken, PostProcessing, PostSessionTokens, PreProcessing, Session, + SetupMandate, Void, }, refunds::{Execute, RSync}, webhooks::VerifyWebhookSource, @@ -19,10 +20,10 @@ use hyperswitch_domain_models::{ CompleteAuthorizeData, ConnectorCustomerData, DefendDisputeRequestData, MandateRevokeRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, - PaymentsPostProcessingData, PaymentsPreProcessingData, PaymentsSessionData, - PaymentsSyncData, PaymentsTaxCalculationData, RefundsData, RetrieveFileRequestData, - SetupMandateRequestData, SubmitEvidenceRequestData, UploadFileRequestData, - VerifyWebhookSourceRequestData, + PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, + PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, RefundsData, + RetrieveFileRequestData, SetupMandateRequestData, SubmitEvidenceRequestData, + UploadFileRequestData, VerifyWebhookSourceRequestData, }, router_response_types::{ AcceptDisputeResponse, DefendDisputeResponse, MandateRevokeResponseData, @@ -58,6 +59,12 @@ pub type PaymentsAuthorizeType = /// Type alias for `ConnectorIntegration` pub type PaymentsTaxCalculationType = dyn ConnectorIntegration; +/// Type alias for `ConnectorIntegration` +pub type PaymentsPostSessionTokensType = dyn ConnectorIntegration< + PostSessionTokens, + PaymentsPostSessionTokensData, + PaymentsResponseData, +>; /// Type alias for `ConnectorIntegration` pub type SetupMandateType = dyn ConnectorIntegration; diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index c28eb291592d..28a7f44c7a87 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -82,6 +82,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::payment_link::payment_link_retrieve, routes::payments::payments_external_authentication, routes::payments::payments_complete_authorize, + routes::payments::payments_post_session_tokens, // Routes for refunds routes::refunds::refunds_create, @@ -646,6 +647,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::WalletResponseData, api_models::payments::PaymentsDynamicTaxCalculationResponse, api_models::payments::DisplayAmountOnSdk, + api_models::payments::PaymentsPostSessionTokensRequest, + api_models::payments::PaymentsPostSessionTokensResponse, )), modifiers(&SecurityAddon) )] diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index aa82dafbe471..b6a81efda09e 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -583,6 +583,24 @@ pub fn payments_complete_authorize() {} pub fn payments_dynamic_tax_calculation() {} +/// Payments - Post Session Tokens +/// +/// +#[utoipa::path( + post, + path = "/payments/{payment_id}/post_session_tokens", + request_body=PaymentsPostSessionTokensRequest, + responses( + (status = 200, description = "Post Session Token is done", body = PaymentsPostSessionTokensResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Create Post Session Tokens for a Payment", + security(("publishable_key" = [])) +)] + +pub fn payments_post_session_tokens() {} + /// Payments - Create Intent /// /// **Creates a payment intent object when amount_details are passed.** diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 67cf28e1a6ae..e94ad375776e 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -9239,8 +9239,79 @@ impl Default for super::settings::RequiredFields { enums::Connector::Paypal, RequiredFieldFinal { mandate: HashMap::new(), - non_mandate: HashMap::new(), - common: HashMap::new(), + non_mandate: HashMap::new( + ), + common: HashMap::from( + [ + ( + "shipping.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.first_name".to_string(), + display_name: "shipping_first_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.last_name".to_string(), + display_name: "shipping_last_name".to_string(), + field_type: enums::FieldType::UserShippingName, + value: None, + } + ), + ( + "shipping.address.city".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserShippingAddressCity, + value: None, + } + ), + ( + "shipping.address.state".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserShippingAddressState, + value: None, + } + ), + ( + "shipping.address.zip".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserShippingAddressPincode, + value: None, + } + ), + ( + "shipping.address.country".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserShippingAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "shipping.address.line1".to_string(), + RequiredFieldInfo { + required_field: "shipping.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserShippingAddressLine1, + value: None, + } + ), + ] + ), } ), ]), diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 5ea880c16b92..254278944e7f 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -15,18 +15,20 @@ use router_env::{instrument, tracing}; use transformers as paypal; use self::transformers::{auth_headers, PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; -use super::utils::{ConnectorErrorType, PaymentsCompleteAuthorizeRequestData}; +use super::utils::{ + ConnectorErrorType, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData, +}; use crate::{ configs::settings, - connector::{ - utils as connector_utils, - utils::{to_connector_meta, ConnectorErrorTypeMapping, RefundsRequestData}, + connector::utils::{ + self as connector_utils, to_connector_meta, ConnectorErrorTypeMapping, RefundsRequestData, }, consts, core::{ errors::{self, CustomResult}, payments, }, + db::domain, events::connector_api_logs::ConnectorEvent, headers, services::{ @@ -71,6 +73,7 @@ impl api::Refund for Paypal {} impl api::RefundExecute for Paypal {} impl api::RefundSync for Paypal {} impl api::ConnectorVerifyWebhookSource for Paypal {} +impl api::PaymentPostSessionTokens for Paypal {} impl api::Payouts for Paypal {} #[cfg(feature = "payouts")] @@ -649,6 +652,94 @@ impl } } +impl + ConnectorIntegration< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsPostSessionTokensRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + fn get_url( + &self, + _req: &types::PaymentsPostSessionTokensRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}v2/checkout/orders", self.base_url(connectors))) + } + fn build_request( + &self, + req: &types::PaymentsPostSessionTokensRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPostSessionTokensType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsPostSessionTokensType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsPostSessionTokensType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsPostSessionTokensRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.amount, + req.request.currency, + )?; + let connector_router_data = paypal::PaypalRouterData::try_from((amount, req))?; + let connector_req = paypal::PaypalPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn handle_response( + &self, + data: &types::PaymentsPostSessionTokensRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalRedirectResponse = res + .response + .parse_struct("PaypalRedirectResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.get_order_error_response(res, event_builder) + } +} + impl ConnectorIntegration for Paypal { @@ -666,10 +757,26 @@ impl ConnectorIntegration CustomResult { - Ok(format!("{}v2/checkout/orders", self.base_url(connectors))) + match &req.request.payment_method_data { + domain::PaymentMethodData::Wallet(domain::WalletData::PaypalSdk( + paypal_wallet_data, + )) => { + let authorize_url = if req.request.is_auto_capture()? { + "capture".to_string() + } else { + "authorize".to_string() + }; + Ok(format!( + "{}v2/checkout/orders/{}/{authorize_url}", + self.base_url(connectors), + paypal_wallet_data.token + )) + } + _ => Ok(format!("{}v2/checkout/orders", self.base_url(connectors))), + } } fn get_request_body( @@ -692,8 +799,20 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() + let payment_method_data = req.request.payment_method_data.clone(); + let req = match payment_method_data { + domain::PaymentMethodData::Wallet(domain::WalletData::PaypalSdk(_)) => { + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .build() + } + _ => services::RequestBuilder::new() .method(services::Method::Post) .url(&types::PaymentsAuthorizeType::get_url( self, req, connectors, @@ -705,7 +824,9 @@ impl ConnectorIntegration { event_builder.map(|i| i.set_response_body(&response)); @@ -733,14 +853,11 @@ impl ConnectorIntegration { event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); - types::RouterData::foreign_try_from(( - types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }, - payment_method_data, - )) + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } PaypalAuthResponse::PaypalThreeDsResponse(response) => { event_builder.map(|i| i.set_response_body(&response)); diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index b51357a42af6..eafc86343b34 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -12,7 +12,7 @@ use url::Url; use crate::{ connector::utils::{ self, to_connector_meta, AccessTokenRequestInfo, AddressDetailsData, CardData, - PaymentsAuthorizeRequestData, RouterData, + PaymentsAuthorizeRequestData, PaymentsPostSessionTokensRequestData, RouterData, }, consts, core::errors, @@ -91,6 +91,21 @@ impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for OrderReque } } +impl From<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> for OrderRequestAmount { + fn from(item: &PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>) -> Self { + Self { + currency_code: item.router_data.request.currency, + value: item.amount.clone(), + breakdown: AmountBreakdown { + item_total: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.clone(), + }, + }, + } + } +} + #[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct AmountBreakdown { item_total: OrderAmount, @@ -136,6 +151,22 @@ impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ItemDetail } } +impl From<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> for ItemDetails { + fn from(item: &PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>) -> Self { + Self { + name: format!( + "Payment for invoice {}", + item.router_data.connector_request_reference_id + ), + quantity: 1, + unit_amount: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.clone(), + }, + } + } +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct Address { address_line_1: Option>, @@ -165,6 +196,21 @@ impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ShippingAd } } +impl From<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> for ShippingAddress { + fn from(item: &PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>) -> Self { + Self { + address: get_address_info(item.router_data.get_optional_shipping()), + name: Some(ShippingName { + full_name: item + .router_data + .get_optional_shipping() + .and_then(|inner_data| inner_data.address.as_ref()) + .and_then(|inner_data| inner_data.first_name.clone()), + }), + } + } +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct ShippingName { full_name: Option>, @@ -366,6 +412,55 @@ fn get_payee(auth_type: &PaypalAuthType) -> Option { }) } +impl TryFrom<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> + for PaypalPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>, + ) -> Result { + let intent = if item.router_data.request.is_auto_capture()? { + PaypalPaymentIntent::Capture + } else { + PaypalPaymentIntent::Authorize + }; + let paypal_auth: PaypalAuthType = + PaypalAuthType::try_from(&item.router_data.connector_auth_type)?; + let payee = get_payee(&paypal_auth); + + let amount = OrderRequestAmount::from(item); + let connector_request_reference_id = + item.router_data.connector_request_reference_id.clone(); + + let shipping_address = ShippingAddress::from(item); + let item_details = vec![ItemDetails::from(item)]; + + let purchase_units = vec![PurchaseUnitRequest { + reference_id: Some(connector_request_reference_id.clone()), + custom_id: item.router_data.request.merchant_order_reference_id.clone(), + invoice_id: Some(connector_request_reference_id), + amount, + payee, + shipping: Some(shipping_address), + items: item_details, + }]; + let payment_source = Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { + experience_context: ContextStruct { + return_url: None, + cancel_url: None, + shipping_preference: ShippingPreference::GetFromFile, + user_action: Some(UserAction::PayNow), + }, + })); + + Ok(Self { + intent, + purchase_units, + payment_source, + }) + } +} + impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalPaymentsRequest { type Error = error_stack::Report; fn try_from( @@ -1040,6 +1135,7 @@ pub struct PaypalMeta { pub capture_id: Option, pub psync_flow: PaypalPaymentIntent, pub next_action: Option, + pub order_id: Option, } fn get_id_based_on_intent( @@ -1094,6 +1190,7 @@ impl capture_id: Some(id), psync_flow: item.response.intent.clone(), next_action: None, + order_id: None, }), types::ResponseId::ConnectorTransactionId(item.response.id.clone()), ), @@ -1104,6 +1201,7 @@ impl capture_id: None, psync_flow: item.response.intent.clone(), next_action: None, + order_id: None, }), types::ResponseId::ConnectorTransactionId(item.response.id.clone()), ), @@ -1252,6 +1350,7 @@ impl capture_id: None, psync_flow: item.response.intent, next_action, + order_id: None, }); let purchase_units = item.response.purchase_units.first(); Ok(Self { @@ -1276,18 +1375,24 @@ impl } } -impl - ForeignTryFrom<( - types::ResponseRouterData, - domain::PaymentMethodData, - )> for types::RouterData +impl + TryFrom< + types::ResponseRouterData< + api::Authorize, + PaypalRedirectResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::PaymentsAuthorizeRouterData { type Error = error_stack::Report; - fn foreign_try_from( - (item, payment_method_data): ( - types::ResponseRouterData, - domain::PaymentMethodData, - ), + fn try_from( + item: types::ResponseRouterData< + api::Authorize, + PaypalRedirectResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, ) -> Result { let status = storage_enums::AttemptStatus::foreign_from(( item.response.clone().status, @@ -1295,21 +1400,12 @@ impl )); let link = get_redirect_url(item.response.links.clone())?; - // For Paypal SDK flow, we need to trigger SDK client and then complete authorize - let next_action = - if let domain::PaymentMethodData::Wallet(domain::WalletData::PaypalSdk(_)) = - payment_method_data - { - Some(api_models::payments::NextActionCall::CompleteAuthorize) - } else { - None - }; - let connector_meta = serde_json::json!(PaypalMeta { authorize_id: None, capture_id: None, psync_flow: item.response.intent, - next_action, + next_action: None, + order_id: None, }); let purchase_units = item.response.purchase_units.first(); Ok(Self { @@ -1334,6 +1430,59 @@ impl } } +impl + TryFrom< + types::ResponseRouterData< + api::PostSessionTokens, + PaypalRedirectResponse, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + >, + > for types::PaymentsPostSessionTokensRouterData +{ + type Error = error_stack::Report; + + fn try_from( + item: types::ResponseRouterData< + api::PostSessionTokens, + PaypalRedirectResponse, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + >, + ) -> Result { + let status = storage_enums::AttemptStatus::foreign_from(( + item.response.clone().status, + item.response.intent.clone(), + )); + + // For Paypal SDK flow, we need to trigger SDK client and then Confirm + let next_action = Some(api_models::payments::NextActionCall::Confirm); + + let connector_meta = serde_json::json!(PaypalMeta { + authorize_id: None, + capture_id: None, + psync_flow: item.response.intent, + next_action, + order_id: Some(item.response.id.clone()), + }); + + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: Some(connector_meta), + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} + impl TryFrom< types::ResponseRouterData< @@ -1396,6 +1545,7 @@ impl capture_id: None, psync_flow: PaypalPaymentIntent::Authenticate, // when there is no capture or auth id present next_action: None, + order_id: None, }); let status = storage_enums::AttemptStatus::foreign_from(( @@ -1818,6 +1968,7 @@ impl TryFrom> capture_id: Some(item.response.id.clone()), psync_flow: PaypalPaymentIntent::Capture, next_action: None, + order_id: None, })), network_txn_id: None, connector_response_reference_id: item diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index ea19acb76dae..34397b5db6df 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1121,6 +1121,20 @@ impl PaymentsSyncRequestData for types::PaymentsSyncData { } } +pub trait PaymentsPostSessionTokensRequestData { + fn is_auto_capture(&self) -> Result; +} + +impl PaymentsPostSessionTokensRequestData for types::PaymentsPostSessionTokensData { + fn is_auto_capture(&self) -> Result { + match self.capture_method { + Some(enums::CaptureMethod::Automatic) | None => Ok(true), + Some(enums::CaptureMethod::Manual) => Ok(false), + Some(_) => Err(errors::ConnectorError::CaptureMethodNotSupported.into()), + } + } +} + #[cfg(feature = "payouts")] pub trait CustomerDetails { fn get_customer_id(&self) -> Result; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f148bcdfbfa9..aa38b142b7c2 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -56,8 +56,8 @@ use time; #[cfg(feature = "v1")] pub use self::operations::{ PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, - PaymentIncrementalAuthorization, PaymentReject, PaymentSession, PaymentSessionUpdate, - PaymentStatus, PaymentUpdate, + PaymentIncrementalAuthorization, PaymentPostSessionTokens, PaymentReject, PaymentSession, + PaymentSessionUpdate, PaymentStatus, PaymentUpdate, }; use self::{ conditional_configs::perform_decision_management, @@ -3637,6 +3637,7 @@ where "PaymentReject" => true, "PaymentSession" => true, "PaymentSessionUpdate" => true, + "PaymentPostSessionTokens" => true, "PaymentIncrementalAuthorization" => matches!( payment_data.get_payment_intent().status, storage_enums::IntentStatus::RequiresCapture diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index e61d7badb6df..3ca620e9e815 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -46,6 +46,8 @@ mod dummy_connector_default_impl { impl api::PaymentSessionUpdateV2 for connector::DummyConnector {} + impl api::PaymentPostSessionTokensV2 for connector::DummyConnector {} + impl services::ConnectorIntegrationV2< api::Authorize, @@ -203,6 +205,15 @@ mod dummy_connector_default_impl { > for connector::DummyConnector { } + impl + services::ConnectorIntegrationV2< + api::PostSessionTokens, + types::PaymentFlowData, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > for connector::DummyConnector + { + } impl services::ConnectorIntegrationV2< @@ -581,6 +592,7 @@ macro_rules! default_imp_for_new_connector_integration_payment { impl api::PaymentsPostProcessingV2 for $path::$connector{} impl api::TaxCalculationV2 for $path::$connector{} impl api::PaymentSessionUpdateV2 for $path::$connector{} + impl api::PaymentPostSessionTokensV2 for $path::$connector{} impl services::ConnectorIntegrationV2 for $path::$connector{} @@ -666,6 +678,13 @@ macro_rules! default_imp_for_new_connector_integration_payment { types::SdkPaymentsSessionUpdateData, types::PaymentsResponseData, > for $path::$connector{} + + impl services::ConnectorIntegrationV2< + api::PostSessionTokens, + types::PaymentFlowData, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > for $path::$connector{} )* }; } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 1b340682c292..c888dc74ce08 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -4,6 +4,7 @@ pub mod cancel_flow; pub mod capture_flow; pub mod complete_authorize_flow; pub mod incremental_authorization_flow; +pub mod post_session_tokens_flow; pub mod psync_flow; pub mod reject_flow; pub mod session_flow; @@ -3060,3 +3061,84 @@ default_imp_for_session_update!( connector::Zen, connector::Zsl ); + +macro_rules! default_imp_for_post_session_tokens { + ($($path:ident::$connector:ident),*) => { + $( impl api::PaymentPostSessionTokens for $path::$connector {} + impl + services::ConnectorIntegration< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData + > for $path::$connector + {} + )* + }; +} +#[cfg(feature = "dummy_connector")] +impl api::PaymentPostSessionTokens for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_post_session_tokens!( + connector::Aci, + connector::Adyen, + connector::Adyenplatform, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bamboraapac, + connector::Bankofamerica, + connector::Billwerk, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Checkout, + connector::Cybersource, + connector::Datatrans, + connector::Ebanx, + connector::Forte, + connector::Globalpay, + connector::Gocardless, + connector::Gpayments, + connector::Iatapay, + connector::Itaubank, + connector::Klarna, + connector::Mifinity, + connector::Multisafepay, + connector::Netcetera, + connector::Nexinets, + connector::Nuvei, + connector::Nmi, + connector::Noon, + connector::Opayo, + connector::Opennode, + connector::Paybox, + connector::Payeezy, + connector::Payme, + connector::Payone, + connector::Payu, + connector::Placetopay, + connector::Plaid, + connector::Prophetpay, + connector::Rapyd, + connector::Razorpay, + connector::Riskified, + connector::Signifyd, + connector::Stripe, + connector::Shift4, + connector::Threedsecureio, + connector::Trustpay, + connector::Wellsfargo, + connector::Wellsfargopayout, + connector::Wise, + connector::Worldpay, + connector::Zen, + connector::Zsl +); diff --git a/crates/router/src/core/payments/flows/post_session_tokens_flow.rs b/crates/router/src/core/payments/flows/post_session_tokens_flow.rs new file mode 100644 index 000000000000..fe83090d795e --- /dev/null +++ b/crates/router/src/core/payments/flows/post_session_tokens_flow.rs @@ -0,0 +1,131 @@ +use async_trait::async_trait; + +use super::ConstructFlowSpecificData; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + payments::{self, access_token, helpers, transformers, Feature, PaymentData}, + }, + routes::SessionState, + services, + types::{self, api, domain}, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &SessionState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + merchant_recipient_data: Option, + header_payload: Option, + ) -> RouterResult { + Box::pin(transformers::construct_payment_router_data::< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + >( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + )) + .await + } + + async fn get_merchant_recipient_data<'a>( + &self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[async_trait] +impl Feature + for types::RouterData< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > +{ + async fn decide_flows<'a>( + self, + state: &SessionState, + connector: &api::ConnectorData, + call_connector_action: payments::CallConnectorAction, + connector_request: Option, + _business_profile: &domain::Profile, + _header_payload: api_models::payments::HeaderPayload, + ) -> RouterResult { + let connector_integration: services::BoxedPaymentConnectorIntegrationInterface< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &self, + call_connector_action, + connector_request, + ) + .await + .to_payment_failed_response()?; + Ok(resp) + } + + async fn add_access_token<'a>( + &self, + state: &SessionState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + creds_identifier: Option<&str>, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self, creds_identifier) + .await + } + + async fn build_flow_specific_connector_request( + &mut self, + state: &SessionState, + connector: &api::ConnectorData, + call_connector_action: payments::CallConnectorAction, + ) -> RouterResult<(Option, bool)> { + let request = match call_connector_action { + payments::CallConnectorAction::Trigger => { + let connector_integration: services::BoxedPaymentConnectorIntegrationInterface< + api::PostSessionTokens, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + connector_integration + .build_request(self, &state.conf.connectors) + .to_payment_failed_response()? + } + _ => None, + }; + + Ok((request, true)) + } +} diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 053c9b8fb0a9..49b89fd56238 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -957,7 +957,7 @@ fn create_paypal_sdk_session_token( connector: connector.connector_name.to_string(), session_token: paypal_sdk_data.data.client_id, sdk_next_action: payment_types::SdkNextAction { - next_action: payment_types::NextActionCall::Confirm, + next_action: payment_types::NextActionCall::PostSessionTokens, }, }, )), diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index f91ef52fde53..efbe9bc8fcf1 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -11,6 +11,8 @@ pub mod payment_confirm; #[cfg(feature = "v1")] pub mod payment_create; #[cfg(feature = "v1")] +pub mod payment_post_session_tokens; +#[cfg(feature = "v1")] pub mod payment_reject; pub mod payment_response; #[cfg(feature = "v1")] @@ -38,8 +40,9 @@ pub use self::payment_response::PaymentResponse; pub use self::{ payment_approve::PaymentApprove, payment_cancel::PaymentCancel, payment_capture::PaymentCapture, payment_confirm::PaymentConfirm, - payment_create::PaymentCreate, payment_reject::PaymentReject, payment_session::PaymentSession, - payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, + payment_create::PaymentCreate, payment_post_session_tokens::PaymentPostSessionTokens, + payment_reject::PaymentReject, payment_session::PaymentSession, payment_start::PaymentStart, + payment_status::PaymentStatus, payment_update::PaymentUpdate, payments_incremental_authorization::PaymentIncrementalAuthorization, tax_calculation::PaymentSessionUpdate, }; diff --git a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs new file mode 100644 index 000000000000..53fd32acc07b --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs @@ -0,0 +1,291 @@ +use std::marker::PhantomData; + +use api_models::enums::FrmSuggestion; +use async_trait::async_trait; +use common_utils::types::keymanager::KeyManagerState; +use error_stack::ResultExt; +use masking::PeekInterface; +use router_derive::PaymentOperation; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payments::{self, helpers, operations, PaymentData}, + }, + routes::{app::ReqState, SessionState}, + services, + types::{ + api::{self, PaymentIdTypeExt}, + domain, + storage::{self, enums as storage_enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy, PaymentOperation)] +#[operation(operations = "all", flow = "post_session_tokens")] +pub struct PaymentPostSessionTokens; + +type PaymentPostSessionTokensOperation<'b, F> = + BoxedOperation<'b, F, api::PaymentsPostSessionTokensRequest, PaymentData>; + +#[async_trait] +impl GetTracker, api::PaymentsPostSessionTokensRequest> + for PaymentPostSessionTokens +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &api::PaymentIdType, + request: &api::PaymentsPostSessionTokensRequest, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + _auth_flow: services::AuthFlow, + _header_payload: &api::HeaderPayload, + ) -> RouterResult< + operations::GetTrackerResponse< + 'a, + F, + api::PaymentsPostSessionTokensRequest, + PaymentData, + >, + > { + let payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + let db = &*state.store; + let key_manager_state: &KeyManagerState = &state.into(); + let merchant_id = merchant_account.get_id(); + let storage_scheme = merchant_account.storage_scheme; + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &state.into(), + &payment_id, + merchant_id, + key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::authenticate_client_secret(Some(request.client_secret.peek()), &payment_intent)?; + + let mut payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + merchant_id, + payment_intent.active_attempt.get_id().as_str(), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let currency = payment_intent.currency.get_required_value("currency")?; + let amount = payment_attempt.get_total_amount().into(); + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(key_manager_state, key_store, profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id.get_string_repr().to_owned(), + })?; + payment_attempt.payment_method = Some(request.payment_method); + payment_attempt.payment_method_type = Some(request.payment_method_type); + let shipping_address = helpers::get_address_by_id( + state, + payment_intent.shipping_address_id.clone(), + key_store, + &payment_intent.payment_id, + merchant_id, + merchant_account.storage_scheme, + ) + .await?; + + let billing_address = helpers::get_address_by_id( + state, + payment_intent.billing_address_id.clone(), + key_store, + &payment_intent.payment_id, + merchant_id, + merchant_account.storage_scheme, + ) + .await?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + customer_acceptance: None, + token: None, + token_data: None, + setup_mandate: None, + address: payments::PaymentAddress::new( + shipping_address.as_ref().map(From::from), + billing_address.as_ref().map(From::from), + None, + business_profile.use_billing_as_payment_method_billing, + ), + confirm: None, + payment_method_data: None, + payment_method_info: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], + authentication: None, + recurring_details: None, + poll_config: None, + tax_data: None, + }; + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + mandate_type: None, + }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl Domain> + for PaymentPostSessionTokens +{ + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + _state: &SessionState, + _payment_data: &mut PaymentData, + _request: Option, + _merchant_key_store: &domain::MerchantKeyStore, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> errors::CustomResult< + ( + PaymentPostSessionTokensOperation<'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 PaymentData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + PaymentPostSessionTokensOperation<'a, F>, + Option, + Option, + )> { + Ok((Box::new(self), None, None)) + } + + async fn get_connector<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + state: &SessionState, + _request: &api::PaymentsPostSessionTokensRequest, + _payment_intent: &storage::PaymentIntent, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> errors::CustomResult { + helpers::get_connector_default(state, None).await + } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _payment_data: &mut PaymentData, + ) -> errors::CustomResult { + Ok(false) + } +} + +#[async_trait] +impl UpdateTracker, api::PaymentsPostSessionTokensRequest> + for PaymentPostSessionTokens +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + _state: &'b SessionState, + _req_state: ReqState, + payment_data: PaymentData, + _customer: Option, + _storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + _key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: api::HeaderPayload, + ) -> RouterResult<(PaymentPostSessionTokensOperation<'b, F>, PaymentData)> + where + F: 'b + Send, + { + Ok((Box::new(self), payment_data)) + } +} + +impl ValidateRequest> + for PaymentPostSessionTokens +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &api::PaymentsPostSessionTokensRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + PaymentPostSessionTokensOperation<'b, F>, + operations::ValidateResult, + )> { + //payment id is already generated and should be sent in the request + let given_payment_id = request.payment_id.clone(); + + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + payment_id: api::PaymentIdType::PaymentIntentId(given_payment_id), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }, + )) + } +} diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index ac400eaf57ff..eedadc541328 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -50,7 +50,7 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( operations = "post_update_tracker", - flow = "sync_data, cancel_data, authorize_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data,incremental_authorization_data, sdk_session_update_data" + flow = "sync_data, cancel_data, authorize_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data,incremental_authorization_data, sdk_session_update_data, post_session_tokens_data" )] pub struct PaymentResponse; @@ -626,11 +626,8 @@ impl PostUpdateTracker, types::SdkPaymentsSessionUpd _key_store: &domain::MerchantKeyStore, _storage_scheme: enums::MerchantStorageScheme, _locale: &Option, - #[cfg(all(feature = "v1", feature = "dynamic_routing"))] _routable_connector: Vec< - RoutableConnectorChoice, - >, - #[cfg(all(feature = "v1", feature = "dynamic_routing"))] - _business_profile: &domain::Profile, + #[cfg(feature = "dynamic_routing")] _routable_connector: Vec, + #[cfg(feature = "dynamic_routing")] _business_profile: &domain::Profile, ) -> RouterResult> where F: 'b + Send, @@ -687,6 +684,68 @@ impl PostUpdateTracker, types::SdkPaymentsSessionUpd } } +#[cfg(feature = "v1")] +#[async_trait] +impl PostUpdateTracker, types::PaymentsPostSessionTokensData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + db: &'b SessionState, + _payment_id: &api::PaymentIdType, + mut payment_data: PaymentData, + router_data: types::RouterData< + F, + types::PaymentsPostSessionTokensData, + types::PaymentsResponseData, + >, + _key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + _locale: &Option, + #[cfg(all(feature = "v1", feature = "dynamic_routing"))] _routable_connector: Vec< + RoutableConnectorChoice, + >, + #[cfg(all(feature = "v1", feature = "dynamic_routing"))] + _business_profile: &domain::Profile, + ) -> RouterResult> + where + F: 'b + Send, + { + match router_data.response.clone() { + Ok(types::PaymentsResponseData::TransactionResponse { + connector_metadata, .. + }) => { + let m_db = db.clone().store; + let payment_attempt_update = + storage::PaymentAttemptUpdate::PostSessionTokensUpdate { + updated_by: storage_scheme.clone().to_string(), + connector_metadata, + }; + let updated_payment_attempt = m_db + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + payment_data.payment_attempt = updated_payment_attempt; + } + Err(err) => { + logger::error!("Invalid request sent to connector: {:?}", err); + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid request sent to connector".to_string(), + })?; + } + _ => { + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unexpected response in PostSessionTokens flow")?; + } + } + Ok(payment_data) + } +} + #[cfg(feature = "v1")] #[async_trait] impl PostUpdateTracker, types::PaymentsCaptureData> diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 0d8322854f07..5d253f44dd4c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -516,6 +516,42 @@ where } } +#[cfg(feature = "v1")] +impl ToResponse for api::PaymentsPostSessionTokensResponse +where + F: Clone, + Op: Debug, + D: OperationSessionGetters, +{ + fn generate_response( + payment_data: D, + _customer: Option, + _auth_flow: services::AuthFlow, + _base_url: &str, + _operation: Op, + _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, + _connector_http_status_code: Option, + _external_latency: Option, + _is_latency_header_enabled: Option, + ) -> RouterResponse { + let papal_sdk_next_action = + paypal_sdk_next_steps_check(payment_data.get_payment_attempt().clone())?; + let next_action = papal_sdk_next_action.map(|paypal_next_action_data| { + api_models::payments::NextActionData::InvokeSdkClient { + next_action_data: paypal_next_action_data, + } + }); + Ok(services::ApplicationResponse::JsonWithHeaders(( + Self { + payment_id: payment_data.get_payment_intent().payment_id.clone(), + next_action, + status: payment_data.get_payment_intent().status, + }, + vec![], + ))) + } +} + impl ForeignTryFrom<(MinorUnit, Option, Option, Currency)> for api_models::payments::DisplayAmountOnSdk { @@ -2078,6 +2114,45 @@ impl TryFrom> for types::SdkPaymentsSessi } } +#[cfg(feature = "v2")] +impl TryFrom> for types::PaymentsPostSessionTokensData { + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + todo!() + } +} + +#[cfg(feature = "v1")] +impl TryFrom> for types::PaymentsPostSessionTokensData { + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data.clone(); + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .unwrap_or_default(); + let shipping_cost = payment_data + .payment_intent + .shipping_cost + .unwrap_or_default(); + // amount here would include amount, surcharge_amount and shipping_cost + let amount = payment_data.payment_intent.amount + shipping_cost + surcharge_amount; + let merchant_order_reference_id = payment_data + .payment_intent + .merchant_order_reference_id + .clone(); + Ok(Self { + amount, //need to change after we move to connector module + currency: payment_data.currency, + merchant_order_reference_id, + capture_method: payment_data.payment_attempt.capture_method, + }) + } +} + impl TryFrom> for types::PaymentsRejectData { type Error = error_stack::Report; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index c267b113fcdb..823cb25b0049 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -582,6 +582,9 @@ impl Payments { .route(web::get().to(payments_retrieve)) .route(web::post().to(payments_update)), ) + .service( + web::resource("/{payment_id}/post_session_tokens").route(web::post().to(payments_post_session_tokens)), + ) .service( web::resource("/{payment_id}/confirm").route(web::post().to(payments_confirm)), ) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 18211b7fbb08..d47ee3caec57 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -136,7 +136,8 @@ impl From for ApiIdentifier { | Flow::GetExtendedCardInfo | Flow::PaymentsCompleteAuthorize | Flow::PaymentsManualUpdate - | Flow::SessionUpdateTaxCalculation => Self::Payments, + | Flow::SessionUpdateTaxCalculation + | Flow::PaymentsPostSessionTokens => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 3972adeb1167..7410c5ee363b 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -356,6 +356,64 @@ pub async fn payments_update( .await } +#[cfg(feature = "v1")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsPostSessionTokens, payment_id))] +pub async fn payments_post_session_tokens( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsPostSessionTokens; + + let payment_id = path.into_inner(); + let payload = payment_types::PaymentsPostSessionTokensRequest { + payment_id, + ..json_payload.into_inner() + }; + tracing::Span::current().record("payment_id", payload.payment_id.get_string_repr()); + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let locking_action = payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req, req_state| { + payments::payments_core::< + api_types::PostSessionTokens, + payment_types::PaymentsPostSessionTokensResponse, + _, + _, + _, + payments::PaymentData, + >( + state, + req_state, + auth.merchant_account, + auth.profile_id, + auth.key_store, + payments::PaymentPostSessionTokens, + req, + api::AuthFlow::Client, + payments::CallConnectorAction::Trigger, + None, + header_payload.clone(), + ) + }, + &auth::PublishableKeyAuth, + locking_action, + )) + .await +} + #[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirm, payment_id))] pub async fn payments_confirm( @@ -1653,6 +1711,23 @@ impl GetLockingInput for payment_types::PaymentsDynamicTaxCalculationRequest { } } +#[cfg(feature = "v1")] +impl GetLockingInput for payment_types::PaymentsPostSessionTokensRequest { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.payment_id.get_string_repr().to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} + #[cfg(feature = "v1")] impl GetLockingInput for payments::PaymentsRedirectResponseData { fn get_locking_input(&self, flow: F) -> api_locking::LockAction diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index b53a58263791..1c0463da9341 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1249,6 +1249,12 @@ impl Authenticate for api_models::payments::PaymentsDynamicTaxCalculationRequest } } +impl Authenticate for api_models::payments::PaymentsPostSessionTokensRequest { + fn get_client_secret(&self) -> Option<&String> { + Some(self.client_secret.peek()) + } +} + impl Authenticate for api_models::payments::PaymentsRetrieveRequest {} impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 225d6a783fc4..3731d8ee0810 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1803,6 +1803,12 @@ impl ClientSecretFetch for PaymentMethodListRequest { } } +impl ClientSecretFetch for payments::PaymentsPostSessionTokensRequest { + fn get_client_secret(&self) -> Option<&String> { + Some(self.client_secret.peek()) + } +} + #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 11f63d6c0217..6f0448ef7772 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -33,7 +33,8 @@ use hyperswitch_domain_models::router_flow_types::{ payments::{ Approve, Authorize, AuthorizeSessionToken, Balance, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, InitPayment, PSync, - PostProcessing, PreProcessing, Reject, SdkSessionUpdate, Session, SetupMandate, Void, + PostProcessing, PostSessionTokens, PreProcessing, Reject, SdkSessionUpdate, Session, + SetupMandate, Void, }, refunds::{Execute, RSync}, webhooks::VerifyWebhookSource, @@ -58,10 +59,11 @@ pub use hyperswitch_domain_models::{ MultipleCaptureRequestData, PaymentMethodTokenizationData, PaymentsApproveData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, - PaymentsPreProcessingData, PaymentsRejectData, PaymentsSessionData, PaymentsSyncData, - PaymentsTaxCalculationData, RefundsData, ResponseId, RetrieveFileRequestData, - SdkPaymentsSessionUpdateData, SetupMandateRequestData, SubmitEvidenceRequestData, - SyncRequestType, UploadFileRequestData, VerifyWebhookSourceRequestData, + PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsRejectData, + PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, RefundsData, ResponseId, + RetrieveFileRequestData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, + SubmitEvidenceRequestData, SyncRequestType, UploadFileRequestData, + VerifyWebhookSourceRequestData, }, router_response_types::{ AcceptDisputeResponse, CaptureSyncResponse, DefendDisputeResponse, MandateReference, @@ -80,10 +82,10 @@ pub use hyperswitch_interfaces::types::{ AcceptDisputeType, ConnectorCustomerType, DefendDisputeType, IncrementalAuthorizationType, MandateRevokeType, PaymentsAuthorizeType, PaymentsBalanceType, PaymentsCaptureType, PaymentsCompleteAuthorizeType, PaymentsInitType, PaymentsPostProcessingType, - PaymentsPreAuthorizeType, PaymentsPreProcessingType, PaymentsSessionType, PaymentsSyncType, - PaymentsVoidType, RefreshTokenType, RefundExecuteType, RefundSyncType, Response, - RetrieveFileType, SetupMandateType, SubmitEvidenceType, TokenizationType, UploadFileType, - VerifyWebhookSourceType, + PaymentsPostSessionTokensType, PaymentsPreAuthorizeType, PaymentsPreProcessingType, + PaymentsSessionType, PaymentsSyncType, PaymentsVoidType, RefreshTokenType, RefundExecuteType, + RefundSyncType, Response, RetrieveFileType, SetupMandateType, SubmitEvidenceType, + TokenizationType, UploadFileType, VerifyWebhookSourceType, }; #[cfg(feature = "payouts")] pub use hyperswitch_interfaces::types::{ @@ -134,6 +136,9 @@ pub type PaymentsTaxCalculationRouterData = pub type SdkSessionUpdateRouterData = RouterData; +pub type PaymentsPostSessionTokensRouterData = + RouterData; + pub type PaymentsCancelRouterData = RouterData; pub type PaymentsRejectRouterData = RouterData; pub type PaymentsApproveRouterData = RouterData; @@ -382,6 +387,7 @@ impl Capturable for CompleteAuthorizeData { impl Capturable for SetupMandateRequestData {} impl Capturable for PaymentsTaxCalculationData {} impl Capturable for SdkPaymentsSessionUpdateData {} +impl Capturable for PaymentsPostSessionTokensData {} impl Capturable for PaymentsCancelData { fn get_captured_amount(&self, payment_data: &PaymentData) -> Option where diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 575725d48741..aedf91a3ce5e 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -9,7 +9,8 @@ pub use api_models::payments::{ PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, - PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, PaymentsRedirectRequest, + PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, + PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, PaymentsRedirectRequest, PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, @@ -19,21 +20,23 @@ use error_stack::ResultExt; pub use hyperswitch_domain_models::router_flow_types::payments::{ Approve, Authorize, AuthorizeSessionToken, Balance, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, InitPayment, PSync, PaymentMethodToken, - PostProcessing, PreProcessing, Reject, SdkSessionUpdate, Session, SetupMandate, Void, + PostProcessing, PostSessionTokens, PreProcessing, Reject, SdkSessionUpdate, Session, + SetupMandate, Void, }; pub use hyperswitch_interfaces::api::payments::{ ConnectorCustomer, MandateSetup, Payment, PaymentApprove, PaymentAuthorize, - PaymentAuthorizeSessionToken, PaymentCapture, PaymentIncrementalAuthorization, PaymentReject, - PaymentSession, PaymentSessionUpdate, PaymentSync, PaymentToken, PaymentVoid, - PaymentsCompleteAuthorize, PaymentsPostProcessing, PaymentsPreProcessing, TaxCalculation, + PaymentAuthorizeSessionToken, PaymentCapture, PaymentIncrementalAuthorization, + PaymentPostSessionTokens, PaymentReject, PaymentSession, PaymentSessionUpdate, PaymentSync, + PaymentToken, PaymentVoid, PaymentsCompleteAuthorize, PaymentsPostProcessing, + PaymentsPreProcessing, TaxCalculation, }; pub use super::payments_v2::{ ConnectorCustomerV2, MandateSetupV2, PaymentApproveV2, PaymentAuthorizeSessionTokenV2, - PaymentAuthorizeV2, PaymentCaptureV2, PaymentIncrementalAuthorizationV2, PaymentRejectV2, - PaymentSessionUpdateV2, PaymentSessionV2, PaymentSyncV2, PaymentTokenV2, PaymentV2, - PaymentVoidV2, PaymentsCompleteAuthorizeV2, PaymentsPostProcessingV2, PaymentsPreProcessingV2, - TaxCalculationV2, + PaymentAuthorizeV2, PaymentCaptureV2, PaymentIncrementalAuthorizationV2, + PaymentPostSessionTokensV2, PaymentRejectV2, PaymentSessionUpdateV2, PaymentSessionV2, + PaymentSyncV2, PaymentTokenV2, PaymentV2, PaymentVoidV2, PaymentsCompleteAuthorizeV2, + PaymentsPostProcessingV2, PaymentsPreProcessingV2, TaxCalculationV2, }; use crate::core::errors; diff --git a/crates/router/src/types/api/payments_v2.rs b/crates/router/src/types/api/payments_v2.rs index 59f132c0154f..b28cfcf28123 100644 --- a/crates/router/src/types/api/payments_v2.rs +++ b/crates/router/src/types/api/payments_v2.rs @@ -1,7 +1,7 @@ pub use hyperswitch_interfaces::api::payments_v2::{ ConnectorCustomerV2, MandateSetupV2, PaymentApproveV2, PaymentAuthorizeSessionTokenV2, - PaymentAuthorizeV2, PaymentCaptureV2, PaymentIncrementalAuthorizationV2, PaymentRejectV2, - PaymentSessionUpdateV2, PaymentSessionV2, PaymentSyncV2, PaymentTokenV2, PaymentV2, - PaymentVoidV2, PaymentsCompleteAuthorizeV2, PaymentsPostProcessingV2, PaymentsPreProcessingV2, - TaxCalculationV2, + PaymentAuthorizeV2, PaymentCaptureV2, PaymentIncrementalAuthorizationV2, + PaymentPostSessionTokensV2, PaymentRejectV2, PaymentSessionUpdateV2, PaymentSessionV2, + PaymentSyncV2, PaymentTokenV2, PaymentV2, PaymentVoidV2, PaymentsCompleteAuthorizeV2, + PaymentsPostProcessingV2, PaymentsPreProcessingV2, TaxCalculationV2, }; diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index 638e6a506f25..407e910b29d1 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -31,6 +31,8 @@ pub enum Derives { IncrementalAuthorizationData, SdkSessionUpdate, SdkSessionUpdateData, + PostSessionTokens, + PostSessionTokensData, } impl Derives { @@ -113,6 +115,12 @@ impl Conversion { Derives::SdkSessionUpdateData => { syn::Ident::new("SdkPaymentsSessionUpdateData", Span::call_site()) } + Derives::PostSessionTokens => { + syn::Ident::new("PaymentsPostSessionTokensRequest", Span::call_site()) + } + Derives::PostSessionTokensData => { + syn::Ident::new("PaymentsPostSessionTokensData", Span::call_site()) + } } } @@ -434,6 +442,7 @@ pub fn operation_derive_inner(input: DeriveInput) -> syn::Result syn::Result