diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 32e6fb08c3..af74f1c494 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -2,8 +2,9 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::ext_traits::ByteSliceExt; use diesel_models::enums; -use error_stack::{IntoReport, ResultExt}; +use error_stack::ResultExt; use masking::PeekInterface; use transformers as paypal; @@ -19,6 +20,7 @@ use crate::{ errors::{self, CustomResult}, payments, }, + db::StorageInterface, headers, services::{ self, @@ -28,8 +30,7 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt}, - storage::enums as storage_enums, - ErrorResponse, Response, + domain, ErrorResponse, Response, }, utils::{self, BytesExt}, }; @@ -52,24 +53,6 @@ impl api::RefundExecute for Paypal {} impl api::RefundSync for Paypal {} impl Paypal { - pub fn connector_transaction_id( - &self, - payment_method: Option, - connector_meta: &Option, - ) -> CustomResult, errors::ConnectorError> { - match payment_method { - Some(diesel_models::enums::PaymentMethod::Wallet) - | Some(diesel_models::enums::PaymentMethod::BankRedirect) => { - let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?; - Ok(Some(meta.order_id)) - } - _ => { - let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?; - Ok(meta.authorize_id) - } - } - } - pub fn get_order_error_response( &self, res: Response, @@ -454,11 +437,13 @@ impl req: &types::PaymentsCompleteAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult { - let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?; Ok(format!( "{}v2/checkout/orders/{}/capture", self.base_url(connectors), - paypal_meta.order_id + req.request + .connector_transaction_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)? )) } @@ -533,24 +518,31 @@ impl ConnectorIntegration Ok(format!( "{}v2/checkout/orders/{}", self.base_url(connectors), - paypal_meta.order_id - )), - _ => { - let capture_id = req - .request + req.request .connector_transaction_id .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + .change_context(errors::ConnectorError::MissingConnectorTransactionID)? + )), + _ => { let psync_url = match paypal_meta.psync_flow { - transformers::PaypalPaymentIntent::Authorize => format!( - "v2/payments/authorizations/{}", - paypal_meta.authorize_id.unwrap_or_default() - ), + transformers::PaypalPaymentIntent::Authorize => { + let authorize_id = paypal_meta.authorize_id.ok_or( + errors::ConnectorError::RequestEncodingFailedWithReason( + "Missing Authorize id".to_string(), + ), + )?; + format!("v2/payments/authorizations/{authorize_id}",) + } transformers::PaypalPaymentIntent::Capture => { - format!("v2/payments/captures/{}", capture_id) + let capture_id = paypal_meta.capture_id.ok_or( + errors::ConnectorError::RequestEncodingFailedWithReason( + "Missing Capture id".to_string(), + ), + )?; + format!("v2/payments/captures/{capture_id}") } }; - Ok(format!("{}{}", self.base_url(connectors), psync_url)) + Ok(format!("{}{psync_url}", self.base_url(connectors))) } } } @@ -676,7 +668,7 @@ impl ConnectorIntegration CustomResult { - let response: paypal::PaymentCaptureResponse = res + let response: paypal::PaypalCaptureResponse = res .response .parse_struct("Paypal PaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -783,11 +775,16 @@ impl ConnectorIntegration, connectors: &settings::Connectors, ) -> CustomResult { - let id = req.request.connector_transaction_id.clone(); + let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_metadata.clone())?; + let capture_id = paypal_meta.capture_id.ok_or( + errors::ConnectorError::RequestEncodingFailedWithReason( + "Missing Capture id".to_string(), + ), + )?; Ok(format!( "{}v2/payments/captures/{}/refund", self.base_url(connectors), - id, + capture_id, )) } @@ -910,25 +907,76 @@ impl ConnectorIntegration, + _merchant_account: &domain::MerchantAccount, + _connector_label: &str, + _key_store: &domain::MerchantKeyStore, + _object_reference_id: api_models::webhooks::ObjectReferenceId, + ) -> CustomResult { + Ok(false) // Verify webhook source is not implemented for Paypal it requires additional apicall this function needs to be modified once we have a way to verify webhook source + } + fn get_webhook_object_reference_id( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let payload: paypal::PaypalWebhooksBody = + request + .body + .parse_struct("PaypalWebhooksBody") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + match payload.resource { + paypal::PaypalResource::PaypalCardWebhooks(resource) => { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + resource.supplementary_data.related_ids.order_id, + ), + )) + } + paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + resource + .purchase_units + .first() + .map(|unit| unit.reference_id.clone()) + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, + ), + )) + } + paypal::PaypalResource::PaypalRefundWebhooks(resource) => { + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId(resource.id), + )) + } + } } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Ok(api::IncomingWebhookEvent::EventNotSupported) + let payload: paypal::PaypalWebooksEventType = request + .body + .parse_struct("PaypalWebooksEventType") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + Ok(api::IncomingWebhookEvent::from(payload.event_type)) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let details: paypal::PaypalWebhooksBody = request + .body + .parse_struct("PaypalWebooksEventType") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + let res_json = utils::Encode::::encode_to_value(&details) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + Ok(res_json) } } diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index b440eed24b..d274e76696 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -167,9 +167,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { match item.request.payment_method_data { api_models::payments::PaymentMethodData::Card(ref ccard) => { - let intent = match item.request.is_auto_capture()? { - true => PaypalPaymentIntent::Capture, - false => PaypalPaymentIntent::Authorize, + let intent = if item.request.is_auto_capture()? { + PaypalPaymentIntent::Capture + } else { + PaypalPaymentIntent::Authorize }; let amount = OrderAmount { currency_code: item.request.currency, @@ -203,7 +204,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest { } api::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data { api_models::payments::WalletData::PaypalRedirect(_) => { - let intent = PaypalPaymentIntent::Capture; + let intent = if item.request.is_auto_capture()? { + PaypalPaymentIntent::Capture + } else { + Err(errors::ConnectorError::NotImplemented( + "Manual capture method for Paypal wallet".to_string(), + ))? + }; let amount = OrderAmount { currency_code: item.request.currency, value: utils::to_currency_base_unit_with_zero_decimal_check( @@ -235,12 +242,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest { ))?, }, api::PaymentMethodData::BankRedirect(ref bank_redirection_data) => { - let intent = match item.request.is_auto_capture()? { - true => PaypalPaymentIntent::Capture, - false => Err(errors::ConnectorError::FlowNotSupported { + let intent = if item.request.is_auto_capture()? { + PaypalPaymentIntent::Capture + } else { + Err(errors::ConnectorError::FlowNotSupported { flow: "Manual capture method for Bank Redirect".to_string(), connector: "Paypal".to_string(), - })?, + })? }; let amount = OrderAmount { currency_code: item.request.currency, @@ -407,12 +415,13 @@ pub struct PaypalPaymentsSyncResponse { id: String, status: PaypalPaymentStatus, amount: OrderAmount, + supplementary_data: PaypalSupplementaryData, } #[derive(Debug, Serialize, Deserialize)] pub struct PaypalMeta { pub authorize_id: Option, - pub order_id: String, + pub capture_id: Option, pub psync_flow: PaypalPaymentIntent, } @@ -464,19 +473,19 @@ impl PaypalPaymentIntent::Capture => ( serde_json::json!(PaypalMeta { authorize_id: None, - order_id: item.response.id, + capture_id: Some(id), psync_flow: item.response.intent.clone() }), - types::ResponseId::ConnectorTransactionId(id), + types::ResponseId::ConnectorTransactionId(item.response.id), ), PaypalPaymentIntent::Authorize => ( serde_json::json!(PaypalMeta { authorize_id: Some(id), - order_id: item.response.id, + capture_id: None, psync_flow: item.response.intent.clone() }), - types::ResponseId::NoResponseId, + types::ResponseId::ConnectorTransactionId(item.response.id), ), }; //payment collection will always have only one element as we only make one transaction per order. @@ -541,14 +550,14 @@ impl let link = get_redirect_url(item.response.clone())?; let connector_meta = serde_json::json!(PaypalMeta { authorize_id: None, - order_id: item.response.id, + capture_id: None, psync_flow: item.response.intent }); Ok(Self { status, response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::NoResponseId, + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), redirection_data: Some(services::RedirectForm::from(( link.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?, services::Method::Get, @@ -580,7 +589,9 @@ impl Ok(Self { status: storage_enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.supplementary_data.related_ids.order_id, + ), redirection_data: None, mandate_reference: None, connector_metadata: None, @@ -622,6 +633,7 @@ pub enum PaypalPaymentStatus { Captured, Completed, Declined, + Voided, Failed, Pending, Denied, @@ -631,7 +643,7 @@ pub enum PaypalPaymentStatus { } #[derive(Debug, Serialize, Deserialize)] -pub struct PaymentCaptureResponse { +pub struct PaypalCaptureResponse { id: String, status: PaypalPaymentStatus, amount: Option, @@ -650,16 +662,17 @@ impl From for storage_enums::AttemptStatus { PaypalPaymentStatus::Pending => Self::Pending, PaypalPaymentStatus::Denied | PaypalPaymentStatus::Expired => Self::Failure, PaypalPaymentStatus::PartiallyCaptured => Self::PartialCharged, + PaypalPaymentStatus::Voided => Self::Voided, } } } -impl TryFrom> +impl TryFrom> for types::PaymentsCaptureRouterData { type Error = error_stack::Report; fn try_from( - item: types::PaymentsCaptureResponseRouterData, + item: types::PaymentsCaptureResponseRouterData, ) -> Result { let amount_captured = item.data.request.amount_to_capture; let status = storage_enums::AttemptStatus::from(item.response.status); @@ -668,12 +681,14 @@ impl TryFrom> Ok(Self { status, response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId( + item.data.request.connector_transaction_id.clone(), + ), redirection_data: None, mandate_reference: None, connector_metadata: Some(serde_json::json!(PaypalMeta { authorize_id: connector_payment_id.authorize_id, - order_id: item.data.request.connector_transaction_id.clone(), + capture_id: Some(item.response.id), psync_flow: PaypalPaymentIntent::Capture })), network_txn_id: None, @@ -853,3 +868,101 @@ pub struct PaypalAccessTokenErrorResponse { pub error: String, pub error_description: String, } + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalWebhooksBody { + pub event_type: PaypalWebhookEventType, + pub resource: PaypalResource, +} + +#[derive(Deserialize, Debug, Serialize)] +pub enum PaypalWebhookEventType { + #[serde(rename = "PAYMENT.AUTHORIZATION.CREATED")] + PaymentAuthorizationCreated, + #[serde(rename = "PAYMENT.AUTHORIZATION.VOIDED")] + PaymentAuthorizationVoided, + #[serde(rename = "PAYMENT.CAPTURE.DECLINED")] + PaymentCaptureDeclined, + #[serde(rename = "PAYMENT.CAPTURE.COMPLETED")] + PaymentCaptureCompleted, + #[serde(rename = "PAYMENT.CAPTURE.PENDING")] + PaymentCapturePending, + #[serde(rename = "PAYMENT.CAPTURE.REFUNDED")] + PaymentCaptureRefunded, + #[serde(rename = "CHECKOUT.ORDER.APPROVED")] + CheckoutOrderApproved, + #[serde(rename = "CHECKOUT.ORDER.COMPLETED")] + CheckoutOrderCompleted, + #[serde(rename = "CHECKOUT.ORDER.PROCESSED")] + CheckoutOrderProcessed, + #[serde(other)] + Unknown, +} + +#[derive(Deserialize, Debug, Serialize)] +#[serde(untagged)] +pub enum PaypalResource { + PaypalCardWebhooks(Box), + PaypalRedirectsWebhooks(Box), + PaypalRefundWebhooks(Box), +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalRefundWebhooks { + pub id: String, + pub amount: OrderAmount, + pub seller_payable_breakdown: PaypalSellerPayableBreakdown, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalSellerPayableBreakdown { + pub total_refunded_amount: OrderAmount, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalCardWebhooks { + pub supplementary_data: PaypalSupplementaryData, + pub amount: OrderAmount, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalRedirectsWebhooks { + pub purchase_units: Vec, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalWebhooksPurchaseUnits { + pub reference_id: String, + pub amount: OrderAmount, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalSupplementaryData { + pub related_ids: PaypalRelatedIds, +} +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalRelatedIds { + pub order_id: String, +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalWebooksEventType { + pub event_type: PaypalWebhookEventType, +} + +impl From for api::IncomingWebhookEvent { + fn from(event: PaypalWebhookEventType) -> Self { + match event { + PaypalWebhookEventType::PaymentCaptureCompleted + | PaypalWebhookEventType::CheckoutOrderCompleted => Self::PaymentIntentSuccess, + PaypalWebhookEventType::PaymentCapturePending + | PaypalWebhookEventType::CheckoutOrderApproved + | PaypalWebhookEventType::CheckoutOrderProcessed => Self::PaymentIntentProcessing, + PaypalWebhookEventType::PaymentCaptureDeclined => Self::PaymentIntentFailure, + PaypalWebhookEventType::PaymentCaptureRefunded => Self::RefundSuccess, + PaypalWebhookEventType::Unknown + | PaypalWebhookEventType::PaymentAuthorizationCreated + | PaypalWebhookEventType::PaymentAuthorizationVoided => Self::EventNotSupported, + } + } +} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 710468bd4a..0b03061841 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -352,6 +352,7 @@ default_imp_for_connector_request_id!( connector::Opennode, connector::Payeezy, connector::Payme, + connector::Paypal, connector::Payu, connector::Powertranz, connector::Rapyd, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index b9b6250abf..fc77268e0a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -8,7 +8,7 @@ use router_env::{instrument, tracing}; use super::{flows::Feature, PaymentAddress, PaymentData}; use crate::{ configs::settings::{ConnectorRequestReferenceIdConfig, Server}, - connector::{Nexinets, Paypal}, + connector::Nexinets, core::{ errors::{self, RouterResponse, RouterResult}, payments::{self, helpers}, @@ -986,24 +986,6 @@ impl TryFrom> for types::PaymentsSyncData } } -impl api::ConnectorTransactionId for Paypal { - fn connector_transaction_id( - &self, - payment_attempt: storage::PaymentAttempt, - ) -> Result, errors::ApiErrorResponse> { - let payment_method = payment_attempt.payment_method; - let metadata = Self::connector_transaction_id( - self, - payment_method, - &payment_attempt.connector_metadata, - ); - match metadata { - Ok(data) => Ok(data), - _ => Err(errors::ApiErrorResponse::ResourceIdNotFound), - } - } -} - impl api::ConnectorTransactionId for Nexinets { fn connector_transaction_id( &self,