From 7e90031c68c7b93db996ee03e11c56b56a87402b Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:38:44 +0530 Subject: [PATCH] feat(router): implement post_update_tracker for SessionUpdate Flow and add support for session_update_flow for Paypal (#6299) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 5 + api-reference/openapi_spec.json | 5 + crates/api_models/src/payments.rs | 2 + crates/common_enums/src/enums.rs | 38 ++-- .../src/router_request_types.rs | 3 + .../src/router_response_types.rs | 6 +- crates/hyperswitch_interfaces/src/types.rs | 11 +- crates/router/src/connector/paypal.rs | 138 ++++++++++++++- .../src/connector/paypal/transformers.rs | 166 +++++++++++++++++- crates/router/src/core/payments.rs | 36 ++++ crates/router/src/core/payments/flows.rs | 1 - .../payments/operations/payment_approve.rs | 1 + .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_capture.rs | 1 + .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 1 + .../payments/operations/payment_create.rs | 1 + .../operations/payment_post_session_tokens.rs | 1 + .../payments/operations/payment_reject.rs | 1 + .../payments/operations/payment_response.rs | 146 +++++++++------ .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 1 + .../payments/operations/payment_status.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../payments_incremental_authorization.rs | 1 + .../payments/operations/tax_calculation.rs | 84 +++++---- .../router/src/core/payments/transformers.rs | 3 + crates/router/src/types.rs | 6 +- crates/router/tests/connectors/utils.rs | 4 +- 29 files changed, 536 insertions(+), 131 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 9ecfe9cf64..e16a206b11 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -14572,6 +14572,11 @@ }, "payment_method_type": { "$ref": "#/components/schemas/PaymentMethodType" + }, + "session_id": { + "type": "string", + "description": "Session Id", + "nullable": true } } }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 49fe3b6b95..355daf66c9 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -17837,6 +17837,11 @@ }, "payment_method_type": { "$ref": "#/components/schemas/PaymentMethodType" + }, + "session_id": { + "type": "string", + "description": "Session Id", + "nullable": true } } }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 265865b973..674a19f3ef 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4916,6 +4916,8 @@ pub struct PaymentsDynamicTaxCalculationRequest { /// Payment method type #[schema(value_type = PaymentMethodType)] pub payment_method_type: api_enums::PaymentMethodType, + /// Session Id + pub session_id: Option, } #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 4601a818fb..0a87e6c19a 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -404,25 +404,25 @@ pub enum AuthorizationStatus { Unresolved, } -// #[derive( -// Clone, -// Debug, -// Eq, -// PartialEq, -// serde::Deserialize, -// serde::Serialize, -// strum::Display, -// strum::EnumString, -// ToSchema, -// Hash, -// )] -// #[router_derive::diesel_enum(storage_type = "text")] -// #[serde(rename_all = "snake_case")] -// #[strum(serialize_all = "snake_case")] -// pub enum SessionUpdateStatus { -// Success, -// Failure, -// } +#[derive( + Clone, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum SessionUpdateStatus { + Success, + Failure, +} #[derive( Clone, diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index f03ab75af1..d599a2caa7 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -841,6 +841,9 @@ pub struct PaymentsTaxCalculationData { pub struct SdkPaymentsSessionUpdateData { pub order_tax_amount: MinorUnit, pub net_amount: MinorUnit, + pub amount: MinorUnit, + pub currency: storage_enums::Currency, + pub session_id: Option, } #[derive(Debug, Clone)] diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index 79a78efb53..eca56b8c86 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -68,9 +68,9 @@ pub enum PaymentsResponseData { PostProcessingResponse { session_token: Option, }, - // SessionUpdateResponse { - // status: common_enums::SessionUpdateStatus, - // }, + SessionUpdateResponse { + status: common_enums::SessionUpdateStatus, + }, } #[derive(Debug, Clone)] diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs index 81e2086f97..41a1b1ba83 100644 --- a/crates/hyperswitch_interfaces/src/types.rs +++ b/crates/hyperswitch_interfaces/src/types.rs @@ -9,8 +9,8 @@ use hyperswitch_domain_models::{ payments::{ Authorize, AuthorizeSessionToken, Balance, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, IncrementalAuthorization, InitPayment, PSync, - PaymentMethodToken, PostProcessing, PostSessionTokens, PreProcessing, Session, - SetupMandate, Void, + PaymentMethodToken, PostProcessing, PostSessionTokens, PreProcessing, SdkSessionUpdate, + Session, SetupMandate, Void, }, refunds::{Execute, RSync}, webhooks::VerifyWebhookSource, @@ -22,8 +22,8 @@ use hyperswitch_domain_models::{ PaymentsCancelData, PaymentsCaptureData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, PaymentsPostSessionTokensData, PaymentsPreProcessingData, PaymentsSessionData, PaymentsSyncData, PaymentsTaxCalculationData, RefundsData, - RetrieveFileRequestData, SetupMandateRequestData, SubmitEvidenceRequestData, - UploadFileRequestData, VerifyWebhookSourceRequestData, + RetrieveFileRequestData, SdkPaymentsSessionUpdateData, SetupMandateRequestData, + SubmitEvidenceRequestData, UploadFileRequestData, VerifyWebhookSourceRequestData, }, router_response_types::{ AcceptDisputeResponse, DefendDisputeResponse, MandateRevokeResponseData, @@ -65,6 +65,9 @@ pub type PaymentsPostSessionTokensType = dyn ConnectorIntegration< PaymentsPostSessionTokensData, PaymentsResponseData, >; +/// Type alias for `ConnectorIntegration` +pub type SdkSessionUpdateType = + dyn ConnectorIntegration; /// Type alias for `ConnectorIntegration` pub type SetupMandateType = dyn ConnectorIntegration; diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 254278944e..274aab8e50 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -10,6 +10,7 @@ use common_utils::{ use diesel_models::enums; use error_stack::ResultExt; use masking::{ExposeInterface, PeekInterface, Secret}; +use router_env::logger; #[cfg(feature = "payouts")] use router_env::{instrument, tracing}; use transformers as paypal; @@ -74,6 +75,7 @@ impl api::RefundExecute for Paypal {} impl api::RefundSync for Paypal {} impl api::ConnectorVerifyWebhookSource for Paypal {} impl api::PaymentPostSessionTokens for Paypal {} +impl api::PaymentSessionUpdate for Paypal {} impl api::Payouts for Paypal {} #[cfg(feature = "payouts")] @@ -482,7 +484,7 @@ impl ConnectorIntegration for Paypal +{ + fn get_headers( + &self, + req: &types::SdkSessionUpdateRouterData, + 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::SdkSessionUpdateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let session_id = + req.request + .session_id + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "session_id", + })?; + Ok(format!( + "{}v2/checkout/orders/{}", + self.base_url(connectors), + session_id + )) + } + + fn build_request( + &self, + req: &types::SdkSessionUpdateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Patch) + .url(&types::SdkSessionUpdateType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::SdkSessionUpdateType::get_headers( + self, req, connectors, + )?) + .set_body(types::SdkSessionUpdateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn get_request_body( + &self, + req: &types::SdkSessionUpdateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let order_amount = connector_utils::convert_amount( + self.amount_converter, + req.request.amount, + req.request.currency, + )?; + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.net_amount, + req.request.currency, + )?; + let order_tax_amount = connector_utils::convert_amount( + self.amount_converter, + req.request.order_tax_amount, + req.request.currency, + )?; + let connector_router_data = paypal::PaypalRouterData::try_from(( + amount, + Some(order_tax_amount), + Some(order_amount), + req, + ))?; + + let connector_req = paypal::PaypalUpdateOrderRequest::try_from(&connector_router_data)?; + // encode only for for urlencoded things. + Ok(RequestContent::Json(Box::new( + connector_req.get_inner_value(), + ))) + } + + fn handle_response( + &self, + data: &types::SdkSessionUpdateRouterData, + _event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + logger::debug!("Expected zero bytes response, skipped parsing of the response"); + // https://developer.paypal.com/docs/api/orders/v2/#orders_patch + // If 204 status code, then the session was updated successfully. + let status = if res.status_code == 204 { + enums::SessionUpdateStatus::Success + } else { + enums::SessionUpdateStatus::Failure + }; + Ok(types::SdkSessionUpdateRouterData { + response: Ok(types::PaymentsResponseData::SessionUpdateResponse { status }), + ..data.clone() + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + impl ConnectorIntegration for Paypal { @@ -789,7 +915,7 @@ impl ConnectorIntegration { pub amount: StringMajorUnit, + pub order_tax_amount: Option, + pub order_amount: Option, pub router_data: T, } -impl TryFrom<(StringMajorUnit, T)> for PaypalRouterData { +impl + TryFrom<( + StringMajorUnit, + Option, + Option, + T, + )> for PaypalRouterData +{ type Error = error_stack::Report; - fn try_from((amount, item): (StringMajorUnit, T)) -> Result { + fn try_from( + (amount, order_tax_amount, order_amount, item): ( + StringMajorUnit, + Option, + Option, + T, + ), + ) -> Result { Ok(Self { amount, + order_tax_amount, + order_amount, router_data: item, }) } @@ -55,6 +73,8 @@ pub mod auth_headers { pub const PAYPAL_AUTH_ASSERTION: &str = "PayPal-Auth-Assertion"; } +const ORDER_QUANTITY: u16 = 1; + #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "UPPERCASE")] pub enum PaypalPaymentIntent { @@ -86,6 +106,7 @@ impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for OrderReque currency_code: item.router_data.request.currency, value: item.amount.clone(), }, + tax_total: None, }, } } @@ -101,14 +122,46 @@ impl From<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> for Or currency_code: item.router_data.request.currency, value: item.amount.clone(), }, + tax_total: None, }, } } } +impl TryFrom<&PaypalRouterData<&types::SdkSessionUpdateRouterData>> for OrderRequestAmount { + type Error = error_stack::Report; + fn try_from( + item: &PaypalRouterData<&types::SdkSessionUpdateRouterData>, + ) -> Result { + Ok(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.order_amount.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "order_amount", + }, + )?, + }, + tax_total: Some(OrderAmount { + currency_code: item.router_data.request.currency, + value: item.order_tax_amount.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "order_tax_amount", + }, + )?, + }), + }, + }) + } +} + #[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct AmountBreakdown { item_total: OrderAmount, + tax_total: Option, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -133,6 +186,7 @@ pub struct ItemDetails { name: String, quantity: u16, unit_amount: OrderAmount, + tax: Option, } impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ItemDetails { @@ -142,11 +196,12 @@ impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ItemDetail "Payment for invoice {}", item.router_data.connector_request_reference_id ), - quantity: 1, + quantity: ORDER_QUANTITY, unit_amount: OrderAmount { currency_code: item.router_data.request.currency, value: item.amount.clone(), }, + tax: None, } } } @@ -158,15 +213,47 @@ impl From<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> for It "Payment for invoice {}", item.router_data.connector_request_reference_id ), - quantity: 1, + quantity: ORDER_QUANTITY, unit_amount: OrderAmount { currency_code: item.router_data.request.currency, value: item.amount.clone(), }, + tax: None, } } } +impl TryFrom<&PaypalRouterData<&types::SdkSessionUpdateRouterData>> for ItemDetails { + type Error = error_stack::Report; + fn try_from( + item: &PaypalRouterData<&types::SdkSessionUpdateRouterData>, + ) -> Result { + Ok(Self { + name: format!( + "Payment for invoice {}", + item.router_data.connector_request_reference_id + ), + quantity: ORDER_QUANTITY, + unit_amount: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.order_amount.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "order_amount", + }, + )?, + }, + tax: Some(OrderAmount { + currency_code: item.router_data.request.currency, + value: item.order_tax_amount.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "order_tax_amount", + }, + )?, + }), + }) + } +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct Address { address_line_1: Option>, @@ -211,6 +298,40 @@ impl From<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> for Sh } } +#[derive(Debug, Serialize, PartialEq, Eq)] +pub struct PaypalUpdateOrderRequest(Vec); + +impl PaypalUpdateOrderRequest { + pub fn get_inner_value(self) -> Vec { + self.0 + } +} + +#[derive(Debug, Serialize, PartialEq, Eq)] +pub struct Operation { + pub op: PaypalOperationType, + pub path: String, + pub value: Value, +} + +#[derive(Debug, Serialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum PaypalOperationType { + Add, + Remove, + Replace, + Move, + Copy, + Test, +} + +#[derive(Debug, Serialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum Value { + Amount(OrderRequestAmount), + Items(Vec), +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct ShippingName { full_name: Option>, @@ -461,6 +582,39 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsPostSessionTokensRouterData>> } } +impl TryFrom<&PaypalRouterData<&types::SdkSessionUpdateRouterData>> for PaypalUpdateOrderRequest { + type Error = error_stack::Report; + + fn try_from( + item: &PaypalRouterData<&types::SdkSessionUpdateRouterData>, + ) -> Result { + let op = PaypalOperationType::Replace; + + // Create separate paths for amount and items + let reference_id = &item.router_data.connector_request_reference_id; + + let amount_path = format!("/purchase_units/@reference_id=='{}'/amount", reference_id); + let items_path = format!("/purchase_units/@reference_id=='{}'/items", reference_id); + + let amount_value = Value::Amount(OrderRequestAmount::try_from(item)?); + + let items_value = Value::Items(vec![ItemDetails::try_from(item)?]); + + Ok(Self(vec![ + Operation { + op: op.clone(), + path: amount_path, + value: amount_value, + }, + Operation { + op, + path: items_path, + value: items_value, + }, + ])) + } +} + impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalPaymentsRequest { type Error = error_stack::Report; fn try_from( @@ -585,7 +739,6 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | domain::WalletData::GooglePayThirdPartySdk(_) | domain::WalletData::MbWayRedirect(_) | domain::WalletData::MobilePayRedirect(_) - | domain::WalletData::Paze(_) | domain::WalletData::SamsungPay(_) | domain::WalletData::TwintRedirect {} | domain::WalletData::VippsRedirect {} @@ -594,7 +747,8 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | domain::WalletData::WeChatPayQr(_) | domain::WalletData::CashappQr(_) | domain::WalletData::SwishQr(_) - | domain::WalletData::Mifinity(_) => Err(errors::ConnectorError::NotImplemented( + | domain::WalletData::Mifinity(_) + | domain::WalletData::Paze(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Paypal"), ))?, }, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index aa38b142b7..68f318958a 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3488,6 +3488,7 @@ where pub recurring_details: Option, pub poll_config: Option, pub tax_data: Option, + pub session_id: Option, } #[derive(Clone, serde::Serialize, Debug)] @@ -5720,6 +5721,41 @@ pub async fn payments_manual_update( )) } +pub trait PaymentMethodChecker { + fn should_update_in_post_update_tracker(&self) -> bool; + fn should_update_in_update_tracker(&self) -> bool; +} + +#[cfg(feature = "v1")] +impl PaymentMethodChecker for PaymentData { + fn should_update_in_post_update_tracker(&self) -> bool { + let payment_method_type = self + .payment_intent + .tax_details + .as_ref() + .and_then(|tax_details| tax_details.payment_method_type.as_ref().map(|pmt| pmt.pmt)); + + matches!( + payment_method_type, + Some(storage_enums::PaymentMethodType::Paypal) + ) + } + + fn should_update_in_update_tracker(&self) -> bool { + let payment_method_type = self + .payment_intent + .tax_details + .as_ref() + .and_then(|tax_details| tax_details.payment_method_type.as_ref().map(|pmt| pmt.pmt)); + + matches!( + payment_method_type, + Some(storage_enums::PaymentMethodType::ApplePay) + | Some(storage_enums::PaymentMethodType::GooglePay) + ) + } +} + pub trait OperationSessionGetters { fn get_payment_attempt(&self) -> &storage::PaymentAttempt; fn get_payment_intent(&self) -> &storage::PaymentIntent; diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index c888dc74ce..eae41c91e4 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -3041,7 +3041,6 @@ default_imp_for_session_update!( connector::Payeezy, connector::Payme, connector::Payone, - connector::Paypal, connector::Payu, connector::Placetopay, connector::Plaid, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index c57f4c3445..e8814c7e56 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -192,6 +192,7 @@ impl GetTracker, api::PaymentsCaptureRequest> recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index f21ccb2c5e..c84dcc50e6 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -202,6 +202,7 @@ impl GetTracker, api::PaymentsCancelRequest> recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 710d6dd7ce..93a79e2d2e 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -253,6 +253,7 @@ impl GetTracker, api::PaymentsCaptu recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 92e462c78b..e705c64d6e 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -344,6 +344,7 @@ impl GetTracker, api::PaymentsRequest> for Co recurring_details, poll_config: None, tax_data: None, + session_id: None, }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 81451d1aff..5dccea54f5 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -754,6 +754,7 @@ impl GetTracker, api::PaymentsRequest> for Pa recurring_details, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 267eeaa040..a715aadf33 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -562,6 +562,7 @@ impl GetTracker, api::PaymentsRequest> for Pa recurring_details, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { 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 index 53fd32acc0..81702979ac 100644 --- a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs +++ b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs @@ -164,6 +164,7 @@ impl GetTracker, api::PaymentsPostSessionToke recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 040eb92d45..2257ff0402 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -188,6 +188,7 @@ impl GetTracker, PaymentsCancelRequest> for P recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index eedadc5413..2257c42f44 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use api_models::routing::RoutableConnectorChoice; use async_trait::async_trait; -use common_enums::AuthorizationStatus; +use common_enums::{AuthorizationStatus, SessionUpdateStatus}; use common_utils::{ ext_traits::{AsyncExt, Encode}, types::{keymanager::KeyManagerState, MinorUnit}, @@ -25,6 +25,7 @@ use crate::{ core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, mandate, payment_methods, + payment_methods::cards::create_encrypted_data, payments::{ helpers::{ self as payments_helpers, @@ -32,7 +33,7 @@ use crate::{ }, tokenization, types::MultipleCaptureData, - PaymentData, + PaymentData, PaymentMethodChecker, }, utils as core_utils, }, @@ -615,16 +616,16 @@ impl PostUpdateTracker, types::SdkPaymentsSessionUpd { async fn update_tracker<'b>( &'b self, - _db: &'b SessionState, + db: &'b SessionState, _payment_id: &api::PaymentIdType, - payment_data: PaymentData, - _router_data: types::RouterData< + mut payment_data: PaymentData, + router_data: types::RouterData< F, types::SdkPaymentsSessionUpdateData, types::PaymentsResponseData, >, - _key_store: &domain::MerchantKeyStore, - _storage_scheme: enums::MerchantStorageScheme, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, _locale: &Option, #[cfg(feature = "dynamic_routing")] _routable_connector: Vec, #[cfg(feature = "dynamic_routing")] _business_profile: &domain::Profile, @@ -632,53 +633,96 @@ impl PostUpdateTracker, types::SdkPaymentsSessionUpd where F: 'b + Send, { - // let session_update_details = - // payment_data - // .payment_intent - // .tax_details - // .clone() - // .ok_or_else(|| { - // report!(errors::ApiErrorResponse::InternalServerError) - // .attach_printable("missing tax_details in payment_intent") - // })?; + let connector = payment_data + .payment_attempt + .connector + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("connector not found")?; - // let pmt_amount = session_update_details - // .pmt - // .clone() - // .map(|pmt| pmt.order_tax_amount) - // .ok_or(errors::ApiErrorResponse::InternalServerError) - // .attach_printable("Missing tax_details.order_tax_amount")?; + // For PayPal, if we call TaxJar for tax calculation, we need to call the connector again to update the order amount so that we can confirm the updated amount and order details. Therefore, we will store the required changes in the database during the post_update_tracker call. + if payment_data.should_update_in_post_update_tracker() { + match router_data.response.clone() { + Ok(types::PaymentsResponseData::SessionUpdateResponse { status }) => { + if status == SessionUpdateStatus::Success { + let shipping_address = payment_data + .tax_data + .clone() + .map(|tax_data| tax_data.shipping_details); - // let total_amount = MinorUnit::from(payment_data.amount) + pmt_amount; + let shipping_details = shipping_address + .clone() + .async_map(|shipping_details| { + create_encrypted_data(db, key_store, shipping_details) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt shipping details")?; - // // if connector_ call successful -> payment_intent.amount update - // match router_data.response.clone() { - // Err(_) => (None, None), - // Ok(types::PaymentsResponseData::SessionUpdateResponse { status }) => { - // if status == SessionUpdateStatus::Success { - // ( - // Some( - // storage::PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { - // amount: total_amount, - // amount_capturable: total_amount, - // }, - // ), - // Some( - // storage::PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { - // amount: pmt_amount, - // }, - // ), - // ) - // } else { - // (None, None) - // } - // } - // _ => Err(errors::ApiErrorResponse::InternalServerError) - // .attach_printable("unexpected response in session_update flow")?, - // }; + let shipping_address = + payments_helpers::create_or_update_address_for_payment_by_request( + db, + shipping_address.as_ref(), + payment_data.payment_intent.shipping_address_id.as_deref(), + &payment_data.payment_intent.merchant_id, + payment_data.payment_intent.customer_id.as_ref(), + key_store, + &payment_data.payment_intent.payment_id, + storage_scheme, + ) + .await?; - // let _shipping_address = payment_data.address.get_shipping(); - // let _amount = payment_data.amount; + let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::SessionResponseUpdate { + tax_details: payment_data.payment_intent.tax_details.clone().ok_or(errors::ApiErrorResponse::InternalServerError).attach_printable("payment_intent.tax_details not found")?, + shipping_address_id: shipping_address.map(|address| address.address_id), + updated_by: payment_data.payment_intent.updated_by.clone(), + shipping_details, + }; + + let m_db = db.clone().store; + let payment_intent = payment_data.payment_intent.clone(); + let key_manager_state: KeyManagerState = db.into(); + + let updated_payment_intent = m_db + .update_payment_intent( + &key_manager_state, + payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_data.payment_intent = updated_payment_intent; + } else { + router_data.response.map_err(|err| { + errors::ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector, + status_code: err.status_code, + reason: err.reason, + } + })?; + } + } + Err(err) => { + Err(errors::ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector, + status_code: err.status_code, + reason: err.reason, + })?; + } + _ => { + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unexpected response in session_update flow")?; + } + } + } Ok(payment_data) } @@ -1546,7 +1590,7 @@ async fn payment_response_update_tracker( types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => (None, None), - // types::PaymentsResponseData::SessionUpdateResponse { .. } => (None, None), + types::PaymentsResponseData::SessionUpdateResponse { .. } => (None, None), types::PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, } => match payment_data.multiple_capture_data { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 2e37d26cde..d2154f16b6 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -211,6 +211,7 @@ impl GetTracker, api::PaymentsSessionRequest> recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 4b44933f6d..3042b15bfa 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -196,6 +196,7 @@ impl GetTracker, api::PaymentsStartRequest> f recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d28476b67b..47f16786ab 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -505,6 +505,7 @@ async fn get_tracker_for_sync< recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index a436100dc0..45a358ab2d 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -479,6 +479,7 @@ impl GetTracker, api::PaymentsRequest> for Pa recurring_details, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index 758a17d3fa..9d80206b9f 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -170,6 +170,7 @@ impl recurring_details: None, poll_config: None, tax_data: None, + session_id: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/tax_calculation.rs b/crates/router/src/core/payments/operations/tax_calculation.rs index 246485c353..d72d25cdd5 100644 --- a/crates/router/src/core/payments/operations/tax_calculation.rs +++ b/crates/router/src/core/payments/operations/tax_calculation.rs @@ -13,7 +13,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::cards::create_encrypted_data, - payments::{self, helpers, operations, PaymentData}, + payments::{self, helpers, operations, PaymentData, PaymentMethodChecker}, utils as core_utils, }, db::errors::ConnectorErrorExt, @@ -177,6 +177,7 @@ impl GetTracker, api::PaymentsDynamicTaxCalcu recurring_details: None, poll_config: None, tax_data: Some(tax_data), + session_id: request.session_id.clone(), }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), @@ -382,54 +383,61 @@ impl UpdateTracker, api::PaymentsDynamicTaxCalculati where F: 'b + Send, { - let shipping_address = payment_data - .tax_data - .clone() - .map(|tax_data| tax_data.shipping_details); + // For Google Pay and Apple Pay, we don’t need to call the connector again; we can directly confirm the payment after tax_calculation. So, we update the required fields in the database during the update_tracker call. + if payment_data.should_update_in_update_tracker() { + let shipping_address = payment_data + .tax_data + .clone() + .map(|tax_data| tax_data.shipping_details); - let shipping_details = shipping_address - .clone() - .async_map(|shipping_details| create_encrypted_data(state, key_store, shipping_details)) - .await - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt shipping details")?; + let shipping_details = shipping_address + .clone() + .async_map(|shipping_details| { + create_encrypted_data(state, key_store, shipping_details) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt shipping details")?; - let shipping_address = helpers::create_or_update_address_for_payment_by_request( - state, - shipping_address.as_ref(), - payment_data.payment_intent.shipping_address_id.as_deref(), - &payment_data.payment_intent.merchant_id, - payment_data.payment_intent.customer_id.as_ref(), - key_store, - &payment_data.payment_intent.payment_id, - storage_scheme, - ) - .await?; + let shipping_address = helpers::create_or_update_address_for_payment_by_request( + state, + shipping_address.as_ref(), + payment_data.payment_intent.shipping_address_id.as_deref(), + &payment_data.payment_intent.merchant_id, + payment_data.payment_intent.customer_id.as_ref(), + key_store, + &payment_data.payment_intent.payment_id, + storage_scheme, + ) + .await?; - let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::SessionResponseUpdate { + let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::SessionResponseUpdate { tax_details: payment_data.payment_intent.tax_details.clone().ok_or(errors::ApiErrorResponse::InternalServerError).attach_printable("payment_intent.tax_details not found")?, shipping_address_id: shipping_address.map(|address| address.address_id), updated_by: payment_data.payment_intent.updated_by.clone(), shipping_details, }; - let db = &*state.store; - let payment_intent = payment_data.payment_intent.clone(); + let db = &*state.store; + let payment_intent = payment_data.payment_intent.clone(); - let updated_payment_intent = db - .update_payment_intent( - &state.into(), - payment_intent, - payment_intent_update, - key_store, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let updated_payment_intent = db + .update_payment_intent( + &state.into(), + payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - payment_data.payment_intent = updated_payment_intent; - Ok((Box::new(self), payment_data)) + payment_data.payment_intent = updated_payment_intent; + Ok((Box::new(self), payment_data)) + } else { + Ok((Box::new(self), payment_data)) + } } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5d253f44dd..72e6c6b997 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -2110,6 +2110,9 @@ impl TryFrom> for types::SdkPaymentsSessi Ok(Self { net_amount, order_tax_amount, + currency: payment_data.currency, + amount: payment_data.payment_intent.amount, + session_id: payment_data.session_id, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 6f0448ef77..3585715433 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -84,8 +84,8 @@ pub use hyperswitch_interfaces::types::{ PaymentsCompleteAuthorizeType, PaymentsInitType, PaymentsPostProcessingType, PaymentsPostSessionTokensType, PaymentsPreAuthorizeType, PaymentsPreProcessingType, PaymentsSessionType, PaymentsSyncType, PaymentsVoidType, RefreshTokenType, RefundExecuteType, - RefundSyncType, Response, RetrieveFileType, SetupMandateType, SubmitEvidenceType, - TokenizationType, UploadFileType, VerifyWebhookSourceType, + RefundSyncType, Response, RetrieveFileType, SdkSessionUpdateType, SetupMandateType, + SubmitEvidenceType, TokenizationType, UploadFileType, VerifyWebhookSourceType, }; #[cfg(feature = "payouts")] pub use hyperswitch_interfaces::types::{ @@ -168,6 +168,8 @@ pub type PaymentsSessionResponseRouterData = ResponseRouterData; pub type PaymentsInitResponseRouterData = ResponseRouterData; +pub type SdkSessionUpdateResponseRouterData = + ResponseRouterData; pub type PaymentsCaptureResponseRouterData = ResponseRouterData; pub type PaymentsPreprocessingResponseRouterData = diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index c68032369a..5acfd67c54 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -567,7 +567,7 @@ pub trait ConnectorActions: Connector { Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Ok(types::PaymentsResponseData::PostProcessingResponse { .. }) => None, - // Ok(types::PaymentsResponseData::SessionUpdateResponse { .. }) => None, + Ok(types::PaymentsResponseData::SessionUpdateResponse { .. }) => None, Err(_) => None, } } @@ -1079,7 +1079,7 @@ pub fn get_connector_transaction_id( Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Ok(types::PaymentsResponseData::PostProcessingResponse { .. }) => None, - // Ok(types::PaymentsResponseData::SessionUpdateResponse { .. }) => None, + Ok(types::PaymentsResponseData::SessionUpdateResponse { .. }) => None, Err(_) => None, } }