diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index e79912f61ff2..04a5ae6f756b 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -652,19 +652,78 @@ impl types::PaymentsResponseData, > for Paypal { - fn build_request( + fn get_headers( &self, - _req: &types::RouterData< - api::SetupMandate, - types::SetupMandateRequestData, - types::PaymentsResponseData, - >, + req: &types::SetupMandateRouterData, + 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::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}v3/vault/payment-tokens/", + self.base_url(connectors) + )) + } + fn get_request_body( + &self, + req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - Err( - errors::ConnectorError::NotImplemented("Setup Mandate flow for Paypal".to_string()) - .into(), - ) + ) -> CustomResult { + let connector_req = paypal::PaypalZeroMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .set_body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::SetupMandateRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalSetupMandatesResponse = res + .response + .parse_struct("PaypalSetupMandatesResponse") + .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.build_error_response(res, event_builder) } } diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index ed8369607df7..7f2d0d0e608e 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -425,7 +425,7 @@ pub struct RedirectRequest { experience_context: ContextStruct, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ContextStruct { return_url: Option, cancel_url: Option, @@ -433,13 +433,13 @@ pub struct ContextStruct { shipping_preference: ShippingPreference, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum UserAction { #[serde(rename = "PAY_NOW")] PayNow, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum ShippingPreference { #[serde(rename = "SET_PROVIDED_ADDRESS")] SetProvidedAddress, @@ -527,6 +527,132 @@ pub struct PaypalPaymentsRequest { payment_source: Option, } +#[derive(Debug, Serialize)] +pub struct PaypalZeroMandateRequest { + payment_source: ZeroMandateSourceItem, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ZeroMandateSourceItem { + Card(CardMandateRequest), + Paypal(PaypalMandateStruct), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaypalMandateStruct { + experience_context: Option, + usage_type: UsageType, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CardMandateRequest { + billing_address: Option
, + expiry: Option>, + name: Option>, + number: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PaypalSetupMandatesResponse { + id: String, + customer: Customer, + payment_source: ZeroMandateSourceItem, + links: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Customer { + id: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PaypalSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let info_response = item.response; + + let mandate_reference = Some(MandateReference { + connector_mandate_id: Some(info_response.id.clone()), + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + }); + // https://developer.paypal.com/docs/api/payment-tokens/v3/#payment-tokens_create + // If 201 status code, then order is captured, other status codes are handled by the error handler + let status = if item.http_code == 201 { + enums::AttemptStatus::Charged + } else { + enums::AttemptStatus::Failure + }; + Ok(Self { + status, + return_url: None, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(mandate_reference), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(info_response.id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} +impl TryFrom<&types::SetupMandateRouterData> for PaypalZeroMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::SetupMandateRouterData) -> Result { + let payment_source = match item.request.payment_method_data.clone() { + domain::PaymentMethodData::Card(ccard) => { + ZeroMandateSourceItem::Card(CardMandateRequest { + billing_address: get_address_info(item.get_optional_billing()), + expiry: Some(ccard.get_expiry_date_as_yyyymm("-")), + name: item.get_optional_billing_full_name(), + number: Some(ccard.card_number), + }) + } + + domain::PaymentMethodData::Wallet(_) + | domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankDebit(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::MandatePayment + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::RealTimePayment(_) + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::CardToken(_) + | domain::PaymentMethodData::CardDetailsForNetworkTransactionId(_) + | domain::PaymentMethodData::NetworkToken(_) + | domain::PaymentMethodData::OpenBanking(_) + | domain::PaymentMethodData::MobilePayment(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ))? + } + }; + + Ok(Self { payment_source }) + } +} + fn get_address_info(payment_address: Option<&api_models::payments::Address>) -> Option
{ let address = payment_address.and_then(|payment_address| payment_address.address.as_ref()); match address { @@ -973,11 +1099,11 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP )?; let payment_source = match payment_method_type { - enums::PaymentMethodType::Credit => Ok(Some(PaymentSourceItem::Card( - CardRequest::CardVaultStruct(VaultStruct { + enums::PaymentMethodType::Credit | enums::PaymentMethodType::Debit => Ok(Some( + PaymentSourceItem::Card(CardRequest::CardVaultStruct(VaultStruct { vault_id: connector_mandate_id.into(), - }), - ))), + })), + )), enums::PaymentMethodType::Paypal => Ok(Some(PaymentSourceItem::Paypal( PaypalRedirectionRequest::PaypalVaultStruct(VaultStruct { vault_id: connector_mandate_id.into(), @@ -1009,7 +1135,6 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | enums::PaymentMethodType::Cashapp | enums::PaymentMethodType::Dana | enums::PaymentMethodType::DanamonVa - | enums::PaymentMethodType::Debit | enums::PaymentMethodType::DirectCarrierBilling | enums::PaymentMethodType::DuitNow | enums::PaymentMethodType::Efecty diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js b/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js index 269fd3ea0b89..4e7f49b1375e 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Paypal.js @@ -16,6 +16,23 @@ const successfulThreeDSTestCardDetails = { card_cvc: "123", }; +const singleUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + single_use: { + amount: 8000, + currency: "USD", + }, + }, +}; + export const connectorDetails = { card_pm: { PaymentIntent: { @@ -222,14 +239,18 @@ export const connectorDetails = { }, }, ZeroAuthMandate: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, Response: { - status: 501, + status: 200, body: { - error: { - type: "invalid_request", - message: "Setup Mandate flow for Paypal is not implemented", - code: "IR_00", - }, + status: "succeeded", }, }, }, @@ -257,13 +278,37 @@ export const connectorDetails = { }, }, Response: { - status: 501, + status: 200, body: { - error: { - type: "invalid_request", - message: "Setup Mandate flow for Paypal is not implemented", - code: "IR_00", - }, + status: "succeeded", + setup_future_usage: "off_session", + }, + }, + }, + SaveCardConfirmAutoCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentIntentOffSession: { + Request: { + currency: "USD", + amount: 6500, + authentication_type: "no_three_ds", + customer_acceptance: null, + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", }, }, },